Sfoglia il codice sorgente

Merge dev into feat/quickbolus

# Conflicts:
#	Trio.xcodeproj/project.pbxproj
#	Trio/Sources/Modules/Home/View/HomeRootView.swift
Magnus Reintz 3 giorni fa
parent
commit
bfef439fe0
100 ha cambiato i file con 3836 aggiunte e 1454 eliminazioni
  1. 2 2
      Config.xcconfig
  2. 1 1
      G7SensorKit
  3. 1 1
      MedtrumKit
  4. 1 1
      OmnipodKit
  5. 348 73
      Trio.xcodeproj/project.pbxproj
  6. 0 9
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  7. BIN
      Trio/Resources/Sounds/alarm.caf
  8. BIN
      Trio/Resources/Sounds/bloom.caf
  9. BIN
      Trio/Resources/Sounds/bloop.caf
  10. BIN
      Trio/Resources/Sounds/bright_alarm.caf
  11. BIN
      Trio/Resources/Sounds/chime.caf
  12. BIN
      Trio/Resources/Sounds/clear_chimes.caf
  13. BIN
      Trio/Resources/Sounds/critical.caf
  14. BIN
      Trio/Resources/Sounds/crying.caf
  15. BIN
      Trio/Resources/Sounds/dings.caf
  16. BIN
      Trio/Resources/Sounds/high_chimes.caf
  17. BIN
      Trio/Resources/Sounds/honk.caf
  18. BIN
      Trio/Resources/Sounds/minimal.caf
  19. BIN
      Trio/Resources/Sounds/mood_synth.caf
  20. BIN
      Trio/Resources/Sounds/simple.caf
  21. BIN
      Trio/Resources/Sounds/spring.caf
  22. BIN
      Trio/Resources/Sounds/synth.caf
  23. BIN
      Trio/Resources/Sounds/trill.caf
  24. BIN
      Trio/Resources/Sounds/urgent_low.caf
  25. 2 0
      Trio/Resources/Trio.entitlements
  26. 152 20
      Trio/Sources/APS/APSManager.swift
  27. 32 8
      Trio/Sources/APS/CGM/PluginSource.swift
  28. 11 45
      Trio/Sources/APS/DeviceDataManager.swift
  29. 5 0
      Trio/Sources/APS/FetchGlucoseManager.swift
  30. 3 3
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  31. 1 2
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  32. 3 0
      Trio/Sources/Application/TrioApp.swift
  33. 6 0
      Trio/Sources/Assemblies/ServiceAssembly.swift
  34. 29 0
      Trio/Sources/Helpers/TimeInterval+Convenience.swift
  35. 542 4
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  36. 1 47
      Trio/Sources/Logger/Logger.swift
  37. 0 2
      Trio/Sources/Models/DecimalPickerSettings.swift
  38. 34 0
      Trio/Sources/Models/GlucoseAlerts/AlarmSoundCatalog.swift
  39. 33 0
      Trio/Sources/Models/GlucoseAlerts/DayNightOptions.swift
  40. 79 0
      Trio/Sources/Models/GlucoseAlerts/DeviceAlertSeverity.swift
  41. 63 0
      Trio/Sources/Models/GlucoseAlerts/DeviceAlertSeverityConfig.swift
  42. 77 0
      Trio/Sources/Models/GlucoseAlerts/GlucoseAlert.swift
  43. 63 0
      Trio/Sources/Models/GlucoseAlerts/GlucoseAlertConfiguration.swift
  44. 103 0
      Trio/Sources/Models/GlucoseAlerts/GlucoseAlertType.swift
  45. 0 27
      Trio/Sources/Models/GlucoseNotificationsOption.swift
  46. 10 0
      Trio/Sources/Models/GlucoseSourceKey.swift
  47. 11 9
      Trio/Sources/Models/NotificationIdentifiers.swift
  48. 0 46
      Trio/Sources/Models/TrioSettings.swift
  49. 7 0
      Trio/Sources/Modules/AlarmWindows/AlarmWindowsDataFlow.swift
  50. 5 0
      Trio/Sources/Modules/AlarmWindows/AlarmWindowsProvider.swift
  51. 18 0
      Trio/Sources/Modules/AlarmWindows/AlarmWindowsStateModel.swift
  52. 135 0
      Trio/Sources/Modules/AlarmWindows/View/AlarmWindowsRootView.swift
  53. 31 10
      Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift
  54. 27 1
      Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  55. 7 0
      Trio/Sources/Modules/DeviceAlarms/DeviceAlarmsDataFlow.swift
  56. 5 0
      Trio/Sources/Modules/DeviceAlarms/DeviceAlarmsProvider.swift
  57. 8 0
      Trio/Sources/Modules/DeviceAlarms/DeviceAlarmsStateModel.swift
  58. 65 0
      Trio/Sources/Modules/DeviceAlarms/View/AddDeviceAlarmSheet.swift
  59. 129 0
      Trio/Sources/Modules/DeviceAlarms/View/DeviceAlarmEditorView.swift
  60. 226 0
      Trio/Sources/Modules/DeviceAlarms/View/DeviceAlarmsRootView.swift
  61. 7 0
      Trio/Sources/Modules/GlucoseAlerts/GlucoseAlertsDataFlow.swift
  62. 5 0
      Trio/Sources/Modules/GlucoseAlerts/GlucoseAlertsProvider.swift
  63. 36 0
      Trio/Sources/Modules/GlucoseAlerts/GlucoseAlertsStateModel.swift
  64. 87 0
      Trio/Sources/Modules/GlucoseAlerts/View/AddGlucoseAlertSheet.swift
  65. 23 0
      Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmActiveSection.swift
  66. 168 0
      Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmAudioSection.swift
  67. 54 0
      Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmBGSection.swift
  68. 39 0
      Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmEnumMenuPicker.swift
  69. 47 0
      Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmGramsSection.swift
  70. 44 0
      Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmWindowIcon.swift
  71. 191 0
      Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertEditorView.swift
  72. 345 0
      Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertsRootView.swift
  73. 0 5
      Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsDataFlow.swift
  74. 0 3
      Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsProvider.swift
  75. 0 53
      Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  76. 0 389
      Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  77. 3 0
      Trio/Sources/Modules/Home/HomeProvider.swift
  78. 1 1
      Trio/Sources/Modules/Home/HomeStateModel.swift
  79. 5 1
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  80. 2 2
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  81. 0 307
      Trio/Sources/Modules/Main/MainStateModel.swift
  82. 5 1
      Trio/Sources/Modules/Main/View/MainRootView.swift
  83. 0 2
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  84. 1 1
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  85. 13 1
      Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift
  86. 3 1
      Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift
  87. 1 0
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  88. 40 15
      Trio/Sources/Modules/Settings/SettingItems.swift
  89. 38 36
      Trio/Sources/Modules/Settings/View/Subviews/NotificationsView.swift
  90. 0 55
      Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift
  91. 0 5
      Trio/Sources/Modules/Snooze/SnoozeDataFlow.swift
  92. 0 3
      Trio/Sources/Modules/Snooze/SnoozeProvider.swift
  93. 0 44
      Trio/Sources/Modules/Snooze/SnoozeStateModel.swift
  94. 0 158
      Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift
  95. 5 2
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  96. 0 52
      Trio/Sources/Router/Router.swift
  97. 9 6
      Trio/Sources/Router/Screen.swift
  98. 259 0
      Trio/Sources/Services/Alerts/AlertCatalogRegistry.swift
  99. 129 0
      Trio/Sources/Services/Alerts/AlertCatalogVendor.swift
  100. 0 0
      Trio/Sources/Services/Alerts/CGMManagerAlertOwnership.swift

+ 2 - 2
Config.xcconfig

@@ -18,8 +18,8 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.8.3
-APP_DEV_VERSION = 0.8.3.10
+APP_VERSION = 0.8.4
+APP_DEV_VERSION = 0.8.4.3
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 4d0780db06c7c95b3a3bf3cdb2f2838d521e411a
+Subproject commit 0c87905297b70a7f0fa41e87fa0fbe1efff6c29f

+ 1 - 1
MedtrumKit

@@ -1 +1 @@
-Subproject commit 1c2d10fd73478f37773f96facf30ff63dbbc05e1
+Subproject commit 3ad0979a3c8fc36c2d48a76da8ea70ef2132943a

+ 1 - 1
OmnipodKit

@@ -1 +1 @@
-Subproject commit 4d254498cf016806b3e0ec33dd5fbfdf6e2135a6
+Subproject commit b5b95562f73e6d3627e3e20ffe31f8d7f6a4639e

+ 348 - 73
Trio.xcodeproj/project.pbxproj

@@ -8,9 +8,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 /* 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 */; };
 		110AEDEB2C51A0AE00615CC9 /* ShortcutsConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE52C51A0AE00615CC9 /* ShortcutsConfigView.swift */; };
@@ -56,7 +54,6 @@
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.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; };
 		3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0725C9D32E00A708ED /* BaseView.swift */; };
 		3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0825C9D32F00A708ED /* BaseProvider.swift */; };
@@ -97,6 +94,7 @@
 		38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E03261B82FA0094D973 /* ReachabilityManager.swift */; };
 		38192E07261BA9960094D973 /* FetchTreatmentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */; };
 		38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */; };
+		D7C5E1F4A7B91E2F3C5D6E70 /* TimeInterval+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C5E1F4A7B91E2F3C5D6E71 /* TimeInterval+Convenience.swift */; };
 		3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3821ED4B25DD18BA00BC42AD /* Constants.swift */; };
 		382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */; };
 		382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C134A25F14E3700715CE1 /* BGTargets.swift */; };
@@ -159,7 +157,6 @@
 		38DAB280260CBB7F00F74C1A /* PumpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DAB27F260CBB7F00F74C1A /* PumpView.swift */; };
 		38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DAB289260D349500F74C1A /* FetchGlucoseManager.swift */; };
 		38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DF1785276A73D400B3528F /* TagCloudView.swift */; };
-		38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 38DF1788276FC8C400B3528F /* SwiftMessages */; };
 		38DF178D27733E6800B3528F /* snow.sks in Resources */ = {isa = PBXBuildFile; fileRef = 38DF178B27733E6800B3528F /* snow.sks */; };
 		38DF178E27733E6800B3528F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38DF178C27733E6800B3528F /* Assets.xcassets */; };
 		38DF179027733EAD00B3528F /* SnowScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DF178F27733EAD00B3528F /* SnowScene.swift */; };
@@ -179,6 +176,50 @@
 		38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E873FD274F761800975559 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		38E87403274F78C000975559 /* libswiftCoreNFC.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E87402274F78C000975559 /* libswiftCoreNFC.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
 		38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E87407274F9AD000975559 /* UserNotificationsManager.swift */; };
+		BD1179202F4E22C100F90001 /* TrioAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179212F4E22C100F90001 /* TrioAlertManager.swift */; };
+		CA01000000000000000010C2 /* AlertCatalogVendor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA01000000000000000010C1 /* AlertCatalogVendor.swift */; };
+		CA01000000000000000010C4 /* AlertCatalogRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA01000000000000000010C3 /* AlertCatalogRegistry.swift */; };
+		CA01000000000000000010C6 /* CGMManagerAlertOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA01000000000000000010C5 /* CGMManagerAlertOwnership.swift */; };
+		BD1179222F4E22C100F90001 /* TrioModalAlertScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179232F4E22C100F90001 /* TrioModalAlertScheduler.swift */; };
+		BD1179242F4E22C100F90001 /* TrioUserNotificationAlertScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179252F4E22C100F90001 /* TrioUserNotificationAlertScheduler.swift */; };
+		BD1179322F4E22C100F90001 /* CriticalAlertAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179312F4E22C100F90001 /* CriticalAlertAudioPlayer.swift */; };
+		BD1179342F4E22C100F90001 /* Sounds in Resources */ = {isa = PBXBuildFile; fileRef = BD1179332F4E22C100F90001 /* Sounds */; };
+		BD1179282F4E22C100F90001 /* TrioAlertCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179292F4E22C100F90001 /* TrioAlertCategory.swift */; };
+		BD1179402F4E22C100F90001 /* ForecastedGlucoseEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179412F4E22C100F90001 /* ForecastedGlucoseEvaluator.swift */; };
+		BD1179522F4E22C100F90001 /* DayNightOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179512F4E22C100F90001 /* DayNightOptions.swift */; };
+		BD1179542F4E22C100F90001 /* GlucoseAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179532F4E22C100F90001 /* GlucoseAlert.swift */; };
+		BD1179562F4E22C100F90001 /* GlucoseAlertConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179552F4E22C100F90001 /* GlucoseAlertConfiguration.swift */; };
+		BD1179582F4E22C100F90001 /* GlucoseAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179572F4E22C100F90001 /* GlucoseAlertType.swift */; };
+		BD11795A2F4E22C100F90001 /* GlucoseAlertsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179592F4E22C100F90001 /* GlucoseAlertsStore.swift */; };
+		BD11795C2F4E22C100F90001 /* GlucoseAlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11795D2F4E22C100F90001 /* GlucoseAlertCoordinator.swift */; };
+		BD1179632F4E22C100F90001 /* GlucoseAlertsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179642F4E22C100F90001 /* GlucoseAlertsDataFlow.swift */; };
+		BD1179652F4E22C100F90001 /* GlucoseAlertsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179662F4E22C100F90001 /* GlucoseAlertsProvider.swift */; };
+		BD1179672F4E22C100F90001 /* GlucoseAlertsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179682F4E22C100F90001 /* GlucoseAlertsStateModel.swift */; };
+		BD1179692F4E22C100F90001 /* GlucoseAlertsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11796A2F4E22C100F90001 /* GlucoseAlertsRootView.swift */; };
+		BD11796B2F4E22C100F90001 /* GlucoseAlertEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11796C2F4E22C100F90001 /* GlucoseAlertEditorView.swift */; };
+		BD11796D2F4E22C100F90001 /* AddGlucoseAlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11796E2F4E22C100F90001 /* AddGlucoseAlertSheet.swift */; };
+		BD1179712F4E22C100F90001 /* AlarmEnumMenuPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179722F4E22C100F90001 /* AlarmEnumMenuPicker.swift */; };
+		BD1179732F4E22C100F90001 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179742F4E22C100F90001 /* AlarmBGSection.swift */; };
+		CA05000000000000000010C2 /* AlarmGramsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA05000000000000000010C1 /* AlarmGramsSection.swift */; };
+		BD1179772F4E22C100F90001 /* AlarmActiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179782F4E22C100F90001 /* AlarmActiveSection.swift */; };
+		BD1179792F4E22C100F90001 /* AlarmAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11797A2F4E22C100F90001 /* AlarmAudioSection.swift */; };
+		BD11797B2F4E22C100F90001 /* AlarmSoundCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11797C2F4E22C100F90001 /* AlarmSoundCatalog.swift */; };
+		BD11797D2F4E22C100F90001 /* AlarmWindowIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11797E2F4E22C100F90001 /* AlarmWindowIcon.swift */; };
+		BD1179832F4E22C100F90001 /* DeviceAlertSeverity.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179842F4E22C100F90001 /* DeviceAlertSeverity.swift */; };
+		BD1179852F4E22C100F90001 /* DeviceAlertSeverityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179862F4E22C100F90001 /* DeviceAlertSeverityConfig.swift */; };
+		BD1179872F4E22C100F90001 /* DeviceAlertsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179882F4E22C100F90001 /* DeviceAlertsStore.swift */; };
+		BD1179892F4E22C100F90001 /* DeviceAlarmsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11798A2F4E22C100F90001 /* DeviceAlarmsDataFlow.swift */; };
+		BD11798B2F4E22C100F90001 /* DeviceAlarmsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11798C2F4E22C100F90001 /* DeviceAlarmsProvider.swift */; };
+		BD11798D2F4E22C100F90001 /* DeviceAlarmsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11798E2F4E22C100F90001 /* DeviceAlarmsStateModel.swift */; };
+		BD11798F2F4E22C100F90001 /* DeviceAlarmsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179902F4E22C100F90001 /* DeviceAlarmsRootView.swift */; };
+		BD1179912F4E22C100F90001 /* DeviceAlarmEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179922F4E22C100F90001 /* DeviceAlarmEditorView.swift */; };
+		BD1179932F4E22C100F90001 /* AddDeviceAlarmSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179942F4E22C100F90001 /* AddDeviceAlarmSheet.swift */; };
+		BD1179A02F4E22C100F90001 /* AlarmWindowsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179A12F4E22C100F90001 /* AlarmWindowsDataFlow.swift */; };
+		BD1179A22F4E22C100F90001 /* AlarmWindowsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179A32F4E22C100F90001 /* AlarmWindowsProvider.swift */; };
+		BD1179A42F4E22C100F90001 /* AlarmWindowsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179A52F4E22C100F90001 /* AlarmWindowsStateModel.swift */; };
+		BD1179A62F4E22C100F90001 /* AlarmWindowsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179A72F4E22C100F90001 /* AlarmWindowsRootView.swift */; };
+		BD1179AA2F4E22C100F90001 /* NotLoopingMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179AB2F4E22C100F90001 /* NotLoopingMonitor.swift */; };
+		BD1179AC2F4E22C100F90001 /* GlucoseSourceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1179AD2F4E22C100F90001 /* GlucoseSourceKey.swift */; };
 		38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E989DC25F5021400C0CED0 /* PumpStatus.swift */; };
 		38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1B25F52C9300C0CED0 /* Signpost.swift */; };
 		38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1C25F52C9300C0CED0 /* Logger.swift */; };
@@ -194,6 +235,23 @@
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
+		BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A002 /* AlertMuterTests.swift */; };
+		BD11A001000000000000A013 /* TandemKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A013 /* TandemKitAlertEmissionTests.swift */; };
+		BD11A001000000000000A012 /* RileyLinkKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A012 /* RileyLinkKitAlertEmissionTests.swift */; };
+		BD11A001000000000000A011 /* OmnipodKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A011 /* OmnipodKitAlertEmissionTests.swift */; };
+		BD11A001000000000000A010 /* MinimedKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */; };
+		BD11A001000000000000A00F /* MedtrumKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00F /* MedtrumKitAlertEmissionTests.swift */; };
+		BD11A001000000000000A00E /* DanaKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00E /* DanaKitAlertEmissionTests.swift */; };
+		BD11A001000000000000A00D /* AlertCatalogRegistryEntriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00D /* AlertCatalogRegistryEntriesTests.swift */; };
+		BD11A001000000000000A00C /* NotLoopingMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00C /* NotLoopingMonitorTests.swift */; };
+		BD11A001000000000000A00B /* TrioModalAlertSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00B /* TrioModalAlertSchedulerTests.swift */; };
+		BD11A001000000000000A00A /* GlucoseAlertCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00A /* GlucoseAlertCoordinatorTests.swift */; };
+		CA06000000000000000010C2 /* GlucoseAlertsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA06000000000000000010C1 /* GlucoseAlertsStoreTests.swift */; };
+		BD11A001000000000000A009 /* TrioAlertCategoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A009 /* TrioAlertCategoryTests.swift */; };
+		BD11A001000000000000A008 /* GlucoseAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A008 /* GlucoseAlertTests.swift */; };
+		BD11A001000000000000A007 /* GlucoseAlertConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */; };
+		BD11A001000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */; };
+		BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */; };
 		38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */; };
 		38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826925CC82DB001FF17A /* NetworkService.swift */; };
 		38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */; };
@@ -384,19 +442,15 @@
 		6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B1A8D232B14D91700E76752 /* Assets.xcassets */; };
 		6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */; };
-		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
 		6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
 		6EADD581738D64431902AC0A /* (null) in Sources */ = {isa = PBXBuildFile; };
-		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
-		715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */; };
 		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		786B6E8A8E294AE883581154 /* QuickPickBolusesConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D543DE17E0054AF0821411BB /* QuickPickBolusesConfigDataFlow.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
 		7F7B756BE8543965D9FDF1A2 /* HistoryDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */; };
-		8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */; };
 		87F4D02B8D4D4D329BA291D2 /* QuickPickBolusesConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91711E7CA54747EFAD6540EE /* QuickPickBolusesConfigStateModel.swift */; };
 		88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */; };
 		8A91342A2D63D9A1007F8874 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -500,6 +554,7 @@
 		BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */; };
 		BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */; };
 		BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */; };
+		CA03000000000000000010C2 /* AlertCatalogRegistryOmniFaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA03000000000000000010C1 /* AlertCatalogRegistryOmniFaultTests.swift */; };
 		BDA25EE42D260CD500035F34 /* AppleWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */; };
 		BDA25EE62D260D5E00035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE52D260D5800035F34 /* WatchState.swift */; };
 		BDA25EFD2D261C0000035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EFC2D261BF200035F34 /* WatchState.swift */; };
@@ -558,7 +613,6 @@
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
 		CC76E9512BD4812E008BEB61 /* Forecast+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */; };
-		CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* GlucoseNotificationSettingsRootView.swift */; };
 		CE1856F52ADC4858007E39C7 /* AddCarbPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1856F42ADC4858007E39C7 /* AddCarbPresetIntent.swift */; };
 		CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1856F62ADC4869007E39C7 /* CarbPresetIntentRequest.swift */; };
 		CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */; };
@@ -632,6 +686,7 @@
 		DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745252C55526F00211FAC /* SMBSettingsRootView.swift */; };
 		DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745282C55642100211FAC /* SettingInputSection.swift */; };
 		DD17452B2C556E8100211FAC /* SettingInputHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452A2C556E8100211FAC /* SettingInputHintView.swift */; };
+		DD500A712807000000000101 /* SnoozeAlertsSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD500A712807000000000100 /* SnoozeAlertsSheetView.swift */; };
 		DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452D2C55AE4800211FAC /* TargetBehavoirDataFlow.swift */; };
 		DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452F2C55AE5300211FAC /* TargetBehaviorProvider.swift */; };
 		DD1745322C55AE6000211FAC /* TargetBehavoirStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745312C55AE6000211FAC /* TargetBehavoirStateModel.swift */; };
@@ -836,7 +891,6 @@
 		E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */; };
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
-		E3A08AAE59538BC8A8ABE477 /* GlucoseNotificationSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3260468377DA9DB4DEE9AF6D /* GlucoseNotificationSettingsDataFlow.swift */; };
 		E592A3702CEEC01E009A472C /* ContactTrickEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */; };
 		E592A3772CEEC038009A472C /* ContactImageStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3752CEEC038009A472C /* ContactImageStateModel.swift */; };
 		E592A3782CEEC038009A472C /* ContactImageDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3732CEEC038009A472C /* ContactImageDataFlow.swift */; };
@@ -1010,14 +1064,10 @@
 		19F95FF429F10FCF00314DDC /* StatProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatProvider.swift; sourceTree = "<group>"; };
 		19F95FF629F10FEE00314DDC /* StatStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatStateModel.swift; sourceTree = "<group>"; };
 		19F95FF929F1102A00314DDC /* StatRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatRootView.swift; sourceTree = "<group>"; };
-		1CAE81192B118804DCD23034 /* SnoozeProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeProvider.swift; sourceTree = "<group>"; };
 		223EC0494F55A91E3EA69EF4 /* TreatmentsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsStateModel.swift; sourceTree = "<group>"; };
-		22963BD06A9C83959D4914E4 /* GlucoseNotificationSettingsRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsRootView.swift; sourceTree = "<group>"; };
 		2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigRootView.swift; sourceTree = "<group>"; };
 		2EEC5C2927FA4C5DA61A1A40 /* QuickPickBolusesConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesConfigProvider.swift; sourceTree = "<group>"; };
 		2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigDataFlow.swift; sourceTree = "<group>"; };
-		3260468377DA9DB4DEE9AF6D /* GlucoseNotificationSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsDataFlow.swift; sourceTree = "<group>"; };
-		36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeDataFlow.swift; sourceTree = "<group>"; };
 		36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorStateModel.swift; sourceTree = "<group>"; };
 		3811DE0725C9D32E00A708ED /* BaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = "<group>"; };
 		3811DE0825C9D32F00A708ED /* BaseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseProvider.swift; sourceTree = "<group>"; };
@@ -1070,6 +1120,7 @@
 		38192E03261B82FA0094D973 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = "<group>"; };
 		38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTreatmentsManager.swift; sourceTree = "<group>"; };
 		38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvenienceExtensions.swift; sourceTree = "<group>"; };
+		D7C5E1F4A7B91E2F3C5D6E71 /* TimeInterval+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Convenience.swift"; sourceTree = "<group>"; };
 		3821ED4B25DD18BA00BC42AD /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
 		382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinSensitivities.swift; sourceTree = "<group>"; };
 		382C134A25F14E3700715CE1 /* BGTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGTargets.swift; sourceTree = "<group>"; };
@@ -1151,6 +1202,50 @@
 		38E873FD274F761800975559 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; };
 		38E87402274F78C000975559 /* libswiftCoreNFC.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreNFC.tbd; path = usr/lib/swift/libswiftCoreNFC.tbd; sourceTree = SDKROOT; };
 		38E87407274F9AD000975559 /* UserNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationsManager.swift; sourceTree = "<group>"; };
+		BD1179212F4E22C100F90001 /* TrioAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioAlertManager.swift; sourceTree = "<group>"; };
+		CA01000000000000000010C1 /* AlertCatalogVendor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCatalogVendor.swift; sourceTree = "<group>"; };
+		CA01000000000000000010C3 /* AlertCatalogRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCatalogRegistry.swift; sourceTree = "<group>"; };
+		CA01000000000000000010C5 /* CGMManagerAlertOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMManagerAlertOwnership.swift; sourceTree = "<group>"; };
+		BD1179232F4E22C100F90001 /* TrioModalAlertScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioModalAlertScheduler.swift; sourceTree = "<group>"; };
+		BD1179252F4E22C100F90001 /* TrioUserNotificationAlertScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioUserNotificationAlertScheduler.swift; sourceTree = "<group>"; };
+		BD1179312F4E22C100F90001 /* CriticalAlertAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalAlertAudioPlayer.swift; sourceTree = "<group>"; };
+		BD1179332F4E22C100F90001 /* Sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Sounds; sourceTree = "<group>"; };
+		BD1179292F4E22C100F90001 /* TrioAlertCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioAlertCategory.swift; sourceTree = "<group>"; };
+		BD1179412F4E22C100F90001 /* ForecastedGlucoseEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastedGlucoseEvaluator.swift; sourceTree = "<group>"; };
+		BD1179512F4E22C100F90001 /* DayNightOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayNightOptions.swift; sourceTree = "<group>"; };
+		BD1179532F4E22C100F90001 /* GlucoseAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlert.swift; sourceTree = "<group>"; };
+		BD1179552F4E22C100F90001 /* GlucoseAlertConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertConfiguration.swift; sourceTree = "<group>"; };
+		BD1179572F4E22C100F90001 /* GlucoseAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertType.swift; sourceTree = "<group>"; };
+		BD1179592F4E22C100F90001 /* GlucoseAlertsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsStore.swift; sourceTree = "<group>"; };
+		BD11795D2F4E22C100F90001 /* GlucoseAlertCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertCoordinator.swift; sourceTree = "<group>"; };
+		BD1179642F4E22C100F90001 /* GlucoseAlertsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsDataFlow.swift; sourceTree = "<group>"; };
+		BD1179662F4E22C100F90001 /* GlucoseAlertsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsProvider.swift; sourceTree = "<group>"; };
+		BD1179682F4E22C100F90001 /* GlucoseAlertsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsStateModel.swift; sourceTree = "<group>"; };
+		BD11796A2F4E22C100F90001 /* GlucoseAlertsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsRootView.swift; sourceTree = "<group>"; };
+		BD11796C2F4E22C100F90001 /* GlucoseAlertEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertEditorView.swift; sourceTree = "<group>"; };
+		BD11796E2F4E22C100F90001 /* AddGlucoseAlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGlucoseAlertSheet.swift; sourceTree = "<group>"; };
+		BD1179722F4E22C100F90001 /* AlarmEnumMenuPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEnumMenuPicker.swift; sourceTree = "<group>"; };
+		BD1179742F4E22C100F90001 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = "<group>"; };
+		CA05000000000000000010C1 /* AlarmGramsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmGramsSection.swift; sourceTree = "<group>"; };
+		BD1179782F4E22C100F90001 /* AlarmActiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActiveSection.swift; sourceTree = "<group>"; };
+		BD11797A2F4E22C100F90001 /* AlarmAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmAudioSection.swift; sourceTree = "<group>"; };
+		BD11797C2F4E22C100F90001 /* AlarmSoundCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSoundCatalog.swift; sourceTree = "<group>"; };
+		BD11797E2F4E22C100F90001 /* AlarmWindowIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmWindowIcon.swift; sourceTree = "<group>"; };
+		BD1179842F4E22C100F90001 /* DeviceAlertSeverity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertSeverity.swift; sourceTree = "<group>"; };
+		BD1179862F4E22C100F90001 /* DeviceAlertSeverityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertSeverityConfig.swift; sourceTree = "<group>"; };
+		BD1179882F4E22C100F90001 /* DeviceAlertsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertsStore.swift; sourceTree = "<group>"; };
+		BD11798A2F4E22C100F90001 /* DeviceAlarmsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlarmsDataFlow.swift; sourceTree = "<group>"; };
+		BD11798C2F4E22C100F90001 /* DeviceAlarmsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlarmsProvider.swift; sourceTree = "<group>"; };
+		BD11798E2F4E22C100F90001 /* DeviceAlarmsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlarmsStateModel.swift; sourceTree = "<group>"; };
+		BD1179902F4E22C100F90001 /* DeviceAlarmsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlarmsRootView.swift; sourceTree = "<group>"; };
+		BD1179922F4E22C100F90001 /* DeviceAlarmEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlarmEditorView.swift; sourceTree = "<group>"; };
+		BD1179942F4E22C100F90001 /* AddDeviceAlarmSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddDeviceAlarmSheet.swift; sourceTree = "<group>"; };
+		BD1179A12F4E22C100F90001 /* AlarmWindowsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmWindowsDataFlow.swift; sourceTree = "<group>"; };
+		BD1179A32F4E22C100F90001 /* AlarmWindowsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmWindowsProvider.swift; sourceTree = "<group>"; };
+		BD1179A52F4E22C100F90001 /* AlarmWindowsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmWindowsStateModel.swift; sourceTree = "<group>"; };
+		BD1179A72F4E22C100F90001 /* AlarmWindowsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmWindowsRootView.swift; sourceTree = "<group>"; };
+		BD1179AB2F4E22C100F90001 /* NotLoopingMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingMonitor.swift; sourceTree = "<group>"; };
+		BD1179AD2F4E22C100F90001 /* GlucoseSourceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSourceKey.swift; sourceTree = "<group>"; };
 		38E989DC25F5021400C0CED0 /* PumpStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatus.swift; sourceTree = "<group>"; };
 		38E98A1B25F52C9300C0CED0 /* Signpost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Signpost.swift; sourceTree = "<group>"; };
 		38E98A1C25F52C9300C0CED0 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
@@ -1169,6 +1264,23 @@
 		38FCF3ED25E9028E0078B0D1 /* TrioTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TrioTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		38FCF3F125E9028E0078B0D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorageTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A002 /* AlertMuterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMuterTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A013 /* TandemKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TandemKitAlertEmissionTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A012 /* RileyLinkKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkKitAlertEmissionTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A011 /* OmnipodKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodKitAlertEmissionTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimedKitAlertEmissionTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A00F /* MedtrumKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumKitAlertEmissionTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A00E /* DanaKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanaKitAlertEmissionTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A00D /* AlertCatalogRegistryEntriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCatalogRegistryEntriesTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A00C /* NotLoopingMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingMonitorTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A00B /* TrioModalAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioModalAlertSchedulerTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A00A /* GlucoseAlertCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertCoordinatorTests.swift; sourceTree = "<group>"; };
+		CA06000000000000000010C1 /* GlucoseAlertsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsStoreTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A009 /* TrioAlertCategoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioAlertCategoryTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A008 /* GlucoseAlertTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertConfigurationTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastedGlucoseEvaluatorTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertsStoreTests.swift; sourceTree = "<group>"; };
 		38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorage.swift; sourceTree = "<group>"; };
 		38FE826925CC82DB001FF17A /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
 		38FE826C25CC8461001FF17A /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = "<group>"; };
@@ -1342,7 +1454,6 @@
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
-		715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationsOption.swift; sourceTree = "<group>"; };
 		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
 		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>"; };
@@ -1365,7 +1476,6 @@
 		B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidator.swift; sourceTree = "<group>"; };
 		B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidatorTests.swift; sourceTree = "<group>"; };
 		B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsSearchTests.swift; sourceTree = "<group>"; };
-		B5822B15939E719628E9FF7C /* SnoozeRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeRootView.swift; sourceTree = "<group>"; };
 		B6E925122EB3932A0076D719 /* OmnipodKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmnipodKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -1448,6 +1558,7 @@
 		BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolTherapySettingsTests.swift; sourceTree = "<group>"; };
+		CA03000000000000000010C1 /* AlertCatalogRegistryOmniFaultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCatalogRegistryOmniFaultTests.swift; sourceTree = "<group>"; };
 		BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchManager.swift; sourceTree = "<group>"; };
 		BDA25EE52D260D5800035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
 		BDA25EFC2D261BF200035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
@@ -1566,7 +1677,6 @@
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		D543DE17E0054AF0821411BB /* QuickPickBolusesConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesConfigDataFlow.swift; sourceTree = "<group>"; };
 		DB3EBC8BA17C4627BD32C58F /* QuickPickBolusesConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesConfigRootView.swift; sourceTree = "<group>"; };
-		DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsDataFlow.swift; sourceTree = "<group>"; };
 		DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsProvider.swift; sourceTree = "<group>"; };
 		DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1589,6 +1699,7 @@
 		DD1745252C55526F00211FAC /* SMBSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1745282C55642100211FAC /* SettingInputSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInputSection.swift; sourceTree = "<group>"; };
 		DD17452A2C556E8100211FAC /* SettingInputHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInputHintView.swift; sourceTree = "<group>"; };
+		DD500A712807000000000100 /* SnoozeAlertsSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozeAlertsSheetView.swift; sourceTree = "<group>"; };
 		DD17452D2C55AE4800211FAC /* TargetBehavoirDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehavoirDataFlow.swift; sourceTree = "<group>"; };
 		DD17452F2C55AE5300211FAC /* TargetBehaviorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehaviorProvider.swift; sourceTree = "<group>"; };
 		DD1745312C55AE6000211FAC /* TargetBehavoirStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehavoirStateModel.swift; sourceTree = "<group>"; };
@@ -1789,12 +1900,12 @@
 		E06B9119275B5EEA003C04B6 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = "<group>"; };
 		E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; };
 		E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitSample.swift; sourceTree = "<group>"; };
-		E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeStateModel.swift; sourceTree = "<group>"; };
 		E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickEntry.swift; sourceTree = "<group>"; };
 		E592A3712CEEC038009A472C /* ContactImageRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageRootView.swift; sourceTree = "<group>"; };
 		E592A3732CEEC038009A472C /* ContactImageDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageDataFlow.swift; sourceTree = "<group>"; };
 		E592A3742CEEC038009A472C /* ContactImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageProvider.swift; sourceTree = "<group>"; };
 		E592A3752CEEC038009A472C /* ContactImageStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStateModel.swift; sourceTree = "<group>"; };
+		B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsSearchTests.swift; sourceTree = "<group>"; };
 		E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsProvider.swift; sourceTree = "<group>"; };
 		F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = "<group>"; };
 		F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = "<group>"; };
@@ -1850,7 +1961,6 @@
 				CE95BF5F2BA7715800DC3DE3 /* MockKit.framework in Frameworks */,
 				DD17A0292E3FE0BD008E1BF0 /* SwiftJWT in Frameworks */,
 				3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */,
-				38DF1789276FC8C400B3528F /* SwiftMessages in Frameworks */,
 				3B4BA7842D8DBD690069D5B8 /* RileyLinkKit.framework in Frameworks */,
 				CE95BF612BA7715900DC3DE3 /* MockKitUI.framework in Frameworks */,
 				E0CC2C5C275B9F0F00A7BC71 /* HealthKit.framework in Frameworks */,
@@ -1923,14 +2033,6 @@
 			path = QuickPickBolusesConfig;
 			sourceTree = "<group>";
 		};
-		0A67A70F9438DB6586398458 /* View */ = {
-			isa = PBXGroup;
-			children = (
-				B5822B15939E719628E9FF7C /* SnoozeRootView.swift */,
-			);
-			path = View;
-			sourceTree = "<group>";
-		};
 		0D76BBC81CEDC1A0050F45EF /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -2136,17 +2238,6 @@
 			path = View;
 			sourceTree = "<group>";
 		};
-		29B478DF61BF8D270F7D8954 /* Snooze */ = {
-			isa = PBXGroup;
-			children = (
-				36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */,
-				1CAE81192B118804DCD23034 /* SnoozeProvider.swift */,
-				E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */,
-				0A67A70F9438DB6586398458 /* View */,
-			);
-			path = Snooze;
-			sourceTree = "<group>";
-		};
 		34CA4DF169B53D67EF18ED8A /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -2176,7 +2267,9 @@
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
 				C2AA6CF52E1A734A00BF6C16 /* SettingsExport */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
-				F66B236E00924A05D6A9F9DF /* GlucoseNotificationSettings */,
+				BD1179602F4E22C100F90001 /* GlucoseAlerts */,
+				BD11799A2F4E22C100F90001 /* DeviceAlarms */,
+				BD1179A82F4E22C100F90001 /* AlarmWindows */,
 				F90692CD274B99850037068D /* HealthKit */,
 				3811DE2725C9D49500A708ED /* Home */,
 				19E1F7E629D0828B005C8D20 /* IconConfig */,
@@ -2193,7 +2286,6 @@
 				110AEDEA2C51A0AE00615CC9 /* ShortcutsConfig */,
 				08A79EA89BFA498F80F4D6FE /* QuickPickBolusesConfig */,
 				DD17451E2C55520000211FAC /* SMBSettings */,
-				29B478DF61BF8D270F7D8954 /* Snooze */,
 				19F95FF129F10F9C00314DDC /* Stat */,
 				DD17452C2C55AE3500211FAC /* TargetBehavoir */,
 				6517011F19F244F64E1FF14B /* TargetsEditor */,
@@ -2315,6 +2407,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				BD1179262F4E22C100F90001 /* Alerts */,
 				3811DE9225C9D88200A708ED /* Appearance */,
 				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
@@ -2339,6 +2432,56 @@
 			path = Services;
 			sourceTree = "<group>";
 		};
+		BD1179262F4E22C100F90001 /* Alerts */ = {
+			isa = PBXGroup;
+			children = (
+				BD1179312F4E22C100F90001 /* CriticalAlertAudioPlayer.swift */,
+				BD1179412F4E22C100F90001 /* ForecastedGlucoseEvaluator.swift */,
+				BD11795D2F4E22C100F90001 /* GlucoseAlertCoordinator.swift */,
+				BD1179592F4E22C100F90001 /* GlucoseAlertsStore.swift */,
+				BD1179882F4E22C100F90001 /* DeviceAlertsStore.swift */,
+				BD1179AB2F4E22C100F90001 /* NotLoopingMonitor.swift */,
+				BD1179292F4E22C100F90001 /* TrioAlertCategory.swift */,
+				BD1179212F4E22C100F90001 /* TrioAlertManager.swift */,
+				BD1179232F4E22C100F90001 /* TrioModalAlertScheduler.swift */,
+				BD1179252F4E22C100F90001 /* TrioUserNotificationAlertScheduler.swift */,
+				CA01000000000000000010C1 /* AlertCatalogVendor.swift */,
+				CA01000000000000000010C3 /* AlertCatalogRegistry.swift */,
+				CA01000000000000000010C5 /* CGMManagerAlertOwnership.swift */,
+			);
+			path = Alerts;
+			sourceTree = "<group>";
+		};
+		DD7E1E30000000000000000A /* Telemetry */ = {
+			isa = PBXGroup;
+			children = (
+				DD7E1E300000000000000013 /* TelemetryAttestor.swift */,
+				DD7E1E300000000000000001 /* TelemetryClient.swift */,
+			);
+			path = Telemetry;
+			sourceTree = "<group>";
+		};
+		DD7E1E30000000000000000B /* Telemetry */ = {
+			isa = PBXGroup;
+			children = (
+				DD7E1E30000000000000000D /* TelemetryDataFlow.swift */,
+				DD7E1E30000000000000000F /* TelemetryProvider.swift */,
+				DD7E1E300000000000000011 /* TelemetryStateModel.swift */,
+				DD7E1E30000000000000000C /* View */,
+			);
+			path = Telemetry;
+			sourceTree = "<group>";
+		};
+		DD7E1E30000000000000000C /* View */ = {
+			isa = PBXGroup;
+			children = (
+				DD7E1E300000000000000003 /* TelemetryPreviewView.swift */,
+				DD7E1E300000000000000005 /* TelemetryPrivacyView.swift */,
+				DD7E1E300000000000000007 /* TelemetryMigrationSheetView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		3811DE9225C9D88200A708ED /* Appearance */ = {
 			isa = PBXGroup;
 			children = (
@@ -2404,6 +2547,7 @@
 			children = (
 				388E597125AD9CF10019842D /* json */,
 				388E596E25AD96040019842D /* javascript */,
+				BD1179332F4E22C100F90001 /* Sounds */,
 				3811DEC725C9DA7300A708ED /* Trio.entitlements */,
 				388E596425AD948E0019842D /* Info.plist */,
 				8A91342B2D63D9A2007F8874 /* InfoPlist.xcstrings */,
@@ -2574,6 +2718,7 @@
 				DD1745282C55642100211FAC /* SettingInputSection.swift */,
 				48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */,
 				DD17452A2C556E8100211FAC /* SettingInputHintView.swift */,
+				DD500A712807000000000100 /* SnoozeAlertsSheetView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -2634,9 +2779,24 @@
 			path = OpenAPS;
 			sourceTree = "<group>";
 		};
+		BD11795B2F4E22C100F90001 /* GlucoseAlerts */ = {
+			isa = PBXGroup;
+			children = (
+				BD11797C2F4E22C100F90001 /* AlarmSoundCatalog.swift */,
+				BD1179512F4E22C100F90001 /* DayNightOptions.swift */,
+				BD1179532F4E22C100F90001 /* GlucoseAlert.swift */,
+				BD1179552F4E22C100F90001 /* GlucoseAlertConfiguration.swift */,
+				BD1179572F4E22C100F90001 /* GlucoseAlertType.swift */,
+				BD1179842F4E22C100F90001 /* DeviceAlertSeverity.swift */,
+				BD1179862F4E22C100F90001 /* DeviceAlertSeverityConfig.swift */,
+			);
+			path = GlucoseAlerts;
+			sourceTree = "<group>";
+		};
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				BD11795B2F4E22C100F90001 /* GlucoseAlerts */,
 				49C782A62F73D9870062B0DD /* AlertEntry.swift */,
 				DDA40BB92F4DB18100257798 /* AlgorithmGlucose.swift */,
 				3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */,
@@ -2655,7 +2815,7 @@
 				49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
-				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
+				BD1179AD2F4E22C100F90001 /* GlucoseSourceKey.swift */,
 				E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */,
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 				38887CCD25F5725200944304 /* IOBEntry.swift */,
@@ -2740,6 +2900,7 @@
 				49239B422EEA27AD00469145 /* TempTargetCalculations.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
+				D7C5E1F4A7B91E2F3C5D6E71 /* TimeInterval+Convenience.swift */,
 				BD2FF19F2AE29D43005D1C5D /* ToggleStyles.swift */,
 				CEB434E428B8FF5D00B70274 /* UIColor.swift */,
 				FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */,
@@ -2892,12 +3053,30 @@
 				CA02000000000000000010C1 /* DeliveryLimitsSyncTests.swift */,
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
+				BD11A000000000000000A002 /* AlertMuterTests.swift */,
+				BD11A000000000000000A013 /* TandemKitAlertEmissionTests.swift */,
+				BD11A000000000000000A012 /* RileyLinkKitAlertEmissionTests.swift */,
+				BD11A000000000000000A011 /* OmnipodKitAlertEmissionTests.swift */,
+				BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */,
+				BD11A000000000000000A00F /* MedtrumKitAlertEmissionTests.swift */,
+				BD11A000000000000000A00E /* DanaKitAlertEmissionTests.swift */,
+				BD11A000000000000000A00D /* AlertCatalogRegistryEntriesTests.swift */,
+				BD11A000000000000000A00C /* NotLoopingMonitorTests.swift */,
+				BD11A000000000000000A00B /* TrioModalAlertSchedulerTests.swift */,
+				BD11A000000000000000A00A /* GlucoseAlertCoordinatorTests.swift */,
+				CA06000000000000000010C1 /* GlucoseAlertsStoreTests.swift */,
+				BD11A000000000000000A009 /* TrioAlertCategoryTests.swift */,
+				BD11A000000000000000A008 /* GlucoseAlertTests.swift */,
+				BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */,
+				BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */,
+				BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
 				3B5CD2C72D4AECD500CE213C /* OpenAPSSwiftTests */,
 				B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */,
 				BD8FC0532D66186000B95AED /* TestError.swift */,
 				BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */,
+				CA03000000000000000010C1 /* AlertCatalogRegistryOmniFaultTests.swift */,
 			);
 			path = TrioTests;
 			sourceTree = "<group>";
@@ -4265,23 +4444,79 @@
 			path = View;
 			sourceTree = "<group>";
 		};
-		F5DE2E6D7B2133BBD3353DC7 /* View */ = {
+		BD1179602F4E22C100F90001 /* GlucoseAlerts */ = {
+			isa = PBXGroup;
+			children = (
+				BD1179642F4E22C100F90001 /* GlucoseAlertsDataFlow.swift */,
+				BD1179662F4E22C100F90001 /* GlucoseAlertsProvider.swift */,
+				BD1179682F4E22C100F90001 /* GlucoseAlertsStateModel.swift */,
+				BD1179612F4E22C100F90001 /* View */,
+			);
+			path = GlucoseAlerts;
+			sourceTree = "<group>";
+		};
+		BD1179A82F4E22C100F90001 /* AlarmWindows */ = {
 			isa = PBXGroup;
 			children = (
-				22963BD06A9C83959D4914E4 /* GlucoseNotificationSettingsRootView.swift */,
+				BD1179A12F4E22C100F90001 /* AlarmWindowsDataFlow.swift */,
+				BD1179A32F4E22C100F90001 /* AlarmWindowsProvider.swift */,
+				BD1179A52F4E22C100F90001 /* AlarmWindowsStateModel.swift */,
+				BD1179A92F4E22C100F90001 /* View */,
+			);
+			path = AlarmWindows;
+			sourceTree = "<group>";
+		};
+		BD1179A92F4E22C100F90001 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BD1179A72F4E22C100F90001 /* AlarmWindowsRootView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
 		};
-		F66B236E00924A05D6A9F9DF /* GlucoseNotificationSettings */ = {
+		BD11799A2F4E22C100F90001 /* DeviceAlarms */ = {
 			isa = PBXGroup;
 			children = (
-				3260468377DA9DB4DEE9AF6D /* GlucoseNotificationSettingsDataFlow.swift */,
-				E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */,
-				DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */,
-				F5DE2E6D7B2133BBD3353DC7 /* View */,
+				BD11798A2F4E22C100F90001 /* DeviceAlarmsDataFlow.swift */,
+				BD11798C2F4E22C100F90001 /* DeviceAlarmsProvider.swift */,
+				BD11798E2F4E22C100F90001 /* DeviceAlarmsStateModel.swift */,
+				BD11799B2F4E22C100F90001 /* View */,
 			);
-			path = GlucoseNotificationSettings;
+			path = DeviceAlarms;
+			sourceTree = "<group>";
+		};
+		BD11799B2F4E22C100F90001 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BD1179942F4E22C100F90001 /* AddDeviceAlarmSheet.swift */,
+				BD1179922F4E22C100F90001 /* DeviceAlarmEditorView.swift */,
+				BD1179902F4E22C100F90001 /* DeviceAlarmsRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		BD1179612F4E22C100F90001 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BD11796E2F4E22C100F90001 /* AddGlucoseAlertSheet.swift */,
+				BD11796C2F4E22C100F90001 /* GlucoseAlertEditorView.swift */,
+				BD11796A2F4E22C100F90001 /* GlucoseAlertsRootView.swift */,
+				BD1179622F4E22C100F90001 /* Components */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		BD1179622F4E22C100F90001 /* Components */ = {
+			isa = PBXGroup;
+			children = (
+				BD1179782F4E22C100F90001 /* AlarmActiveSection.swift */,
+				BD11797A2F4E22C100F90001 /* AlarmAudioSection.swift */,
+				BD1179742F4E22C100F90001 /* AlarmBGSection.swift */,
+				CA05000000000000000010C1 /* AlarmGramsSection.swift */,
+				BD1179722F4E22C100F90001 /* AlarmEnumMenuPicker.swift */,
+				BD11797E2F4E22C100F90001 /* AlarmWindowIcon.swift */,
+			);
+			path = Components;
 			sourceTree = "<group>";
 		};
 		F75CB57ED6971B46F8756083 /* CGMSettings */ = {
@@ -4360,7 +4595,6 @@
 				3811DE0F25C9D37700A708ED /* Swinject */,
 				38B17B6525DD90E0005CAE3D /* SwiftDate */,
 				3833B46C26012030003021B3 /* Algorithms */,
-				38DF1788276FC8C400B3528F /* SwiftMessages */,
 				CEB434FC28B90B7C00B70274 /* SwiftCharts */,
 				B958F1B62BA0711600484851 /* MKRingProgressView */,
 				3BD9687B2D8DDD4600899469 /* SlideButton */,
@@ -4538,7 +4772,6 @@
 				3811DE0E25C9D37700A708ED /* XCRemoteSwiftPackageReference "Swinject" */,
 				38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */,
 				3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
-				38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */,
 				CEB434FB28B90B7C00B70274 /* XCRemoteSwiftPackageReference "SwiftCharts" */,
 				B958F1B52BA0711600484851 /* XCRemoteSwiftPackageReference "MKRingProgressView" */,
 				3BD9687A2D8DDD4600899469 /* XCRemoteSwiftPackageReference "SlideButton" */,
@@ -4575,6 +4808,7 @@
 				19DA48E829CD339B00EEA1E7 /* Assets.xcassets in Resources */,
 				8A91342A2D63D9A1007F8874 /* Localizable.xcstrings in Resources */,
 				388E596F25AD96040019842D /* javascript in Resources */,
+				BD1179342F4E22C100F90001 /* Sounds in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -4807,6 +5041,7 @@
 				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				DDD5889D2DDDC9A900C8848D /* TimeAgoFormatter.swift in Sources */,
+				D7C5E1F4A7B91E2F3C5D6E70 /* TimeInterval+Convenience.swift in Sources */,
 				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				3B5CD2B72D4AEA6600CE213C /* ComputedBGTargets.swift in Sources */,
 				3B5CD2B82D4AEA6600CE213C /* ComputedInsulinSensitivities.swift in Sources */,
@@ -4848,6 +5083,7 @@
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
 				AC19EF2C94084B5BA0175D1D /* SettingsSearchHighlight.swift in Sources */,
 				DD17452B2C556E8100211FAC /* SettingInputHintView.swift in Sources */,
+				DD500A712807000000000101 /* SnoozeAlertsSheetView.swift in Sources */,
 				38E44528274E401C00EC9A94 /* Protected.swift in Sources */,
 				DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */,
 				3811DEB625C9D88300A708ED /* UnlockManager.swift in Sources */,
@@ -4981,6 +5217,49 @@
 				BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */,
 				DDFF20312DB1D15500AB8A96 /* BluetoothPermissionStepView.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
+				BD1179202F4E22C100F90001 /* TrioAlertManager.swift in Sources */,
+				CA01000000000000000010C2 /* AlertCatalogVendor.swift in Sources */,
+				CA01000000000000000010C4 /* AlertCatalogRegistry.swift in Sources */,
+				CA01000000000000000010C6 /* CGMManagerAlertOwnership.swift in Sources */,
+				BD1179222F4E22C100F90001 /* TrioModalAlertScheduler.swift in Sources */,
+				BD1179242F4E22C100F90001 /* TrioUserNotificationAlertScheduler.swift in Sources */,
+				BD1179322F4E22C100F90001 /* CriticalAlertAudioPlayer.swift in Sources */,
+				BD1179282F4E22C100F90001 /* TrioAlertCategory.swift in Sources */,
+				BD1179402F4E22C100F90001 /* ForecastedGlucoseEvaluator.swift in Sources */,
+				BD1179522F4E22C100F90001 /* DayNightOptions.swift in Sources */,
+				BD1179542F4E22C100F90001 /* GlucoseAlert.swift in Sources */,
+				BD1179562F4E22C100F90001 /* GlucoseAlertConfiguration.swift in Sources */,
+				BD1179582F4E22C100F90001 /* GlucoseAlertType.swift in Sources */,
+				BD11795A2F4E22C100F90001 /* GlucoseAlertsStore.swift in Sources */,
+				BD11795C2F4E22C100F90001 /* GlucoseAlertCoordinator.swift in Sources */,
+				BD1179632F4E22C100F90001 /* GlucoseAlertsDataFlow.swift in Sources */,
+				BD1179652F4E22C100F90001 /* GlucoseAlertsProvider.swift in Sources */,
+				BD1179672F4E22C100F90001 /* GlucoseAlertsStateModel.swift in Sources */,
+				BD1179692F4E22C100F90001 /* GlucoseAlertsRootView.swift in Sources */,
+				BD11796B2F4E22C100F90001 /* GlucoseAlertEditorView.swift in Sources */,
+				BD11796D2F4E22C100F90001 /* AddGlucoseAlertSheet.swift in Sources */,
+				BD1179712F4E22C100F90001 /* AlarmEnumMenuPicker.swift in Sources */,
+				BD1179732F4E22C100F90001 /* AlarmBGSection.swift in Sources */,
+				CA05000000000000000010C2 /* AlarmGramsSection.swift in Sources */,
+				BD1179772F4E22C100F90001 /* AlarmActiveSection.swift in Sources */,
+				BD1179792F4E22C100F90001 /* AlarmAudioSection.swift in Sources */,
+				BD11797B2F4E22C100F90001 /* AlarmSoundCatalog.swift in Sources */,
+				BD11797D2F4E22C100F90001 /* AlarmWindowIcon.swift in Sources */,
+				BD1179832F4E22C100F90001 /* DeviceAlertSeverity.swift in Sources */,
+				BD1179852F4E22C100F90001 /* DeviceAlertSeverityConfig.swift in Sources */,
+				BD1179872F4E22C100F90001 /* DeviceAlertsStore.swift in Sources */,
+				BD1179892F4E22C100F90001 /* DeviceAlarmsDataFlow.swift in Sources */,
+				BD11798B2F4E22C100F90001 /* DeviceAlarmsProvider.swift in Sources */,
+				BD11798D2F4E22C100F90001 /* DeviceAlarmsStateModel.swift in Sources */,
+				BD11798F2F4E22C100F90001 /* DeviceAlarmsRootView.swift in Sources */,
+				BD1179912F4E22C100F90001 /* DeviceAlarmEditorView.swift in Sources */,
+				BD1179932F4E22C100F90001 /* AddDeviceAlarmSheet.swift in Sources */,
+				BD1179A02F4E22C100F90001 /* AlarmWindowsDataFlow.swift in Sources */,
+				BD1179A22F4E22C100F90001 /* AlarmWindowsProvider.swift in Sources */,
+				BD1179A42F4E22C100F90001 /* AlarmWindowsStateModel.swift in Sources */,
+				BD1179A62F4E22C100F90001 /* AlarmWindowsRootView.swift in Sources */,
+				BD1179AA2F4E22C100F90001 /* NotLoopingMonitor.swift in Sources */,
+				BD1179AC2F4E22C100F90001 /* GlucoseSourceKey.swift in Sources */,
 				DD3F1F902D9E153F00DCE7B3 /* NightscoutImportStepView.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
@@ -5235,7 +5514,6 @@
 				BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
-				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
 				BB5227A51D9D4377A1A70BA6 /* TempTargetData.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
@@ -5308,9 +5586,6 @@
 				DD9E6DA22D59A12700514CEC /* MealHistory.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,
-				E3A08AAE59538BC8A8ABE477 /* GlucoseNotificationSettingsDataFlow.swift in Sources */,
-				0F7A65FBD2CD8D6477ED4539 /* GlucoseNotificationSettingsProvider.swift in Sources */,
-				3171D2818C7C72CD1584BB5E /* GlucoseNotificationSettingsStateModel.swift in Sources */,
 				DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */,
 				DD30B9CA2E062A3400DA677C /* ForecastGenerator.swift in Sources */,
 				DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */,
@@ -5355,16 +5630,11 @@
 				DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */,
 				DD4A00212DAEEED800AB7387 /* OnboardingView+AlgorithmUtil.swift in Sources */,
 				DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */,
-				CD78BB94E43B249D60CC1A1B /* GlucoseNotificationSettingsRootView.swift in Sources */,
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,
-				6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */,
-				6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */,
 				DD3C47B52DC57E06003DD20D /* MainMigrationErrorView.swift in Sources */,
 				BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */,
 				DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */,
-				8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */,
 				BDA25EE42D260CD500035F34 /* AppleWatchManager.swift in Sources */,
-				0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */,
 				BD175EBE0000100000000001 /* HistoryDataFlow+Models.swift in Sources */,
 				BD175EBE0000100000000002 /* HistoryDeletionTarget.swift in Sources */,
 				BD175EBE0000100000000003 /* HistoryStateModel+Glucose.swift in Sources */,
@@ -5427,6 +5697,23 @@
 				3B2CE68B2E24ADF7005EF782 /* IobGenerateTests.swift in Sources */,
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
+				BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */,
+				BD11A001000000000000A013 /* TandemKitAlertEmissionTests.swift in Sources */,
+				BD11A001000000000000A012 /* RileyLinkKitAlertEmissionTests.swift in Sources */,
+				BD11A001000000000000A011 /* OmnipodKitAlertEmissionTests.swift in Sources */,
+				BD11A001000000000000A010 /* MinimedKitAlertEmissionTests.swift in Sources */,
+				BD11A001000000000000A00F /* MedtrumKitAlertEmissionTests.swift in Sources */,
+				BD11A001000000000000A00E /* DanaKitAlertEmissionTests.swift in Sources */,
+				BD11A001000000000000A00D /* AlertCatalogRegistryEntriesTests.swift in Sources */,
+				BD11A001000000000000A00C /* NotLoopingMonitorTests.swift in Sources */,
+				BD11A001000000000000A00B /* TrioModalAlertSchedulerTests.swift in Sources */,
+				BD11A001000000000000A00A /* GlucoseAlertCoordinatorTests.swift in Sources */,
+				CA06000000000000000010C2 /* GlucoseAlertsStoreTests.swift in Sources */,
+				BD11A001000000000000A009 /* TrioAlertCategoryTests.swift in Sources */,
+				BD11A001000000000000A008 /* GlucoseAlertTests.swift in Sources */,
+				BD11A001000000000000A007 /* GlucoseAlertConfigurationTests.swift in Sources */,
+				BD11A001000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift in Sources */,
+				BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */,
 				3B8221B22E5882E300585156 /* DetermineBasalEarlyExitTests.swift in Sources */,
 				3BA643EA2ED9FAD8007BC31F /* DetermineBasalAggressiveDosingTests.swift in Sources */,
 				BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */,
@@ -5435,6 +5722,7 @@
 				41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */,
 				B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */,
 				BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */,
+				CA03000000000000000010C2 /* AlertCatalogRegistryOmniFaultTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -6148,14 +6436,6 @@
 				minimumVersion = 6.3.1;
 			};
 		};
-		38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/SwiftKickMobile/SwiftMessages";
-			requirement = {
-				kind = upToNextMajorVersion;
-				minimumVersion = 9.0.0;
-			};
-		};
 		3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
 			isa = XCRemoteSwiftPackageReference;
 			repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git";
@@ -6230,11 +6510,6 @@
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
 			productName = SwiftDate;
 		};
-		38DF1788276FC8C400B3528F /* SwiftMessages */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 38DF1787276FC8C300B3528F /* XCRemoteSwiftPackageReference "SwiftMessages" */;
-			productName = SwiftMessages;
-		};
 		3B47C60F2DA0A28F00B0E5EF /* FirebaseCrashlytics */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 3B47C60E2DA0A28F00B0E5EF /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;

+ 0 - 9
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -254,15 +254,6 @@
       }
     },
     {
-      "identity" : "swiftmessages",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/SwiftKickMobile/SwiftMessages",
-      "state" : {
-        "revision" : "62e12e138fc3eedf88c7553dd5d98712aa119f40",
-        "version" : "9.0.9"
-      }
-    },
-    {
       "identity" : "swinject",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/Swinject/Swinject",

BIN
Trio/Resources/Sounds/alarm.caf


BIN
Trio/Resources/Sounds/bloom.caf


BIN
Trio/Resources/Sounds/bloop.caf


BIN
Trio/Resources/Sounds/bright_alarm.caf


BIN
Trio/Resources/Sounds/chime.caf


BIN
Trio/Resources/Sounds/clear_chimes.caf


BIN
Trio/Resources/Sounds/critical.caf


BIN
Trio/Resources/Sounds/crying.caf


BIN
Trio/Resources/Sounds/dings.caf


BIN
Trio/Resources/Sounds/high_chimes.caf


BIN
Trio/Resources/Sounds/honk.caf


BIN
Trio/Resources/Sounds/minimal.caf


BIN
Trio/Resources/Sounds/mood_synth.caf


BIN
Trio/Resources/Sounds/simple.caf


BIN
Trio/Resources/Sounds/spring.caf


BIN
Trio/Resources/Sounds/synth.caf


BIN
Trio/Resources/Sounds/trill.caf


BIN
Trio/Resources/Sounds/urgent_low.caf


+ 2 - 0
Trio/Resources/Trio.entitlements

@@ -14,6 +14,8 @@
 	<array>
 		<string>TAG</string>
 	</array>
+	<key>com.apple.developer.usernotifications.time-sensitive</key>
+	<true/>
 	<key>com.apple.security.application-groups</key>
 	<array>
 		<string>$(APP_GROUP_ID)</string>

+ 152 - 20
Trio/Sources/APS/APSManager.swift

@@ -8,6 +8,11 @@ import Swinject
 
 protocol APSManager {
     func heartbeat(date: Date)
+    /// Mark the next loop attempt as user-initiated (e.g. force-loop button).
+    /// Surfaces transient errors immediately instead of waiting for the
+    /// usual dwell threshold — when the user explicitly asks for a loop,
+    /// they want feedback even if the underlying error is "transient".
+    func markNextLoopUserInitiated()
     func enactBolus(amount: Double, isSMB: Bool, callback: ((Bool, String) -> Void)?) async
     var pumpManager: PumpManagerUI? { get set }
     var bluetoothManager: BluetoothStateManager? { get }
@@ -36,6 +41,13 @@ protocol APSManager {
     var iobFileDidUpdate: PassthroughSubject<Void, Never> { get }
 }
 
+/// Notified after a bolus-related failure so observing UI (e.g. the
+/// treatment screen's bolus state) can clean up state. Broadcast by
+/// `APSManager` from `enactBolus` / `cancelBolus` error paths.
+protocol BolusFailureObserver {
+    func bolusDidFail()
+}
+
 enum APSError: LocalizedError {
     case pumpError(Error)
     case invalidPumpState(message: String)
@@ -57,17 +69,6 @@ enum APSError: LocalizedError {
             return String(localized: "Manual Temporary Basal Rate (\(message)). Looping suspended.")
         }
     }
-
-    static func pumpErrorMatches(message: String) -> Bool {
-        message.contains(String(localized: "Pump Error"))
-    }
-
-    static func pumpWarningMatches(message: String) -> Bool {
-        message.contains(String(localized: "Invalid Pump State")) || message
-            .contains("PumpMessage") || message
-            .contains("PumpOpsError") || message.contains("RileyLink") || message
-            .contains(String(localized: "Pump did not respond in time"))
-    }
 }
 
 // MARK: - Thread-safe loop serialization
@@ -104,6 +105,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var tddStorage: TDDStorage!
     @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var trioAlertManager: TrioAlertManager!
     @Persisted(key: "lastLoopStartDate") private var lastLoopStartDate: Date = .distantPast
     @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
         didSet {
@@ -261,6 +263,24 @@ final class BaseAPSManager: APSManager, Injectable {
         Task { [weak self] in
             guard let self else { return }
 
+            // Consume the user-initiated flag unconditionally — it was set
+            // for the loop the user just triggered. If the guards below block
+            // (suspended, too-soon, no pump), the next scheduled tick must
+            // not inherit it and bypass dwell suppression for an error the
+            // user didn't request.
+            let userInitiated = self.nextLoopUserInitiated
+            self.nextLoopUserInitiated = false
+
+            // Don't try to run a loop while pump setup / pod pairing is in
+            // progress — `verifyStatus` would throw `invalidPumpState("Pump
+            // not set")` and surface a modal banner on top of the pod
+            // activation sheet, closing the sheet (reported by tester during
+            // O5 pairing).
+            guard self.pumpManager != nil else {
+                debug(.apsManager, "No pump manager — skipping loop attempt")
+                return
+            }
+
             // Atomic check-and-set via actor — eliminates the race between
             // checking isLooping.value and sending isLooping(true).
             guard await loopGuard.tryStart(
@@ -272,6 +292,11 @@ final class BaseAPSManager: APSManager, Injectable {
                 return
             }
 
+            // Affects whether transient errors surface immediately instead of
+            // dwell-suppressed (see `surfaceErrorIfNeeded`).
+            self.currentLoopUserInitiated = userInitiated
+            defer { self.currentLoopUserInitiated = false }
+
             // Start background task
             // we probably need to refactor this when implementing Swift 6 due to mutation of a captured var in an async context
             var taskID: UIBackgroundTaskIdentifier = .invalid
@@ -370,6 +395,8 @@ final class BaseAPSManager: APSManager, Injectable {
             debug(.apsManager, "Loop succeeded")
             lastLoopDate = Date()
             lastError.send(nil)
+            transientCategoryFirstSeen.removeAll()
+            transientCategoryCount.removeAll()
         }
 
         loopStats(loopStatRecord: loopStatRecord)
@@ -594,9 +621,14 @@ final class BaseAPSManager: APSManager, Injectable {
             }
         } catch {
             warning(.apsManager, "Bolus failed with error: \(error)")
-            processError(APSError.pumpError(error))
+            lastError.send(APSError.pumpError(error))
+            issueAlertForCategory(
+                .bolusFailed,
+                title: String(localized: "Bolus failed"),
+                body: String(localized: "Check pump history before repeating.")
+                    + "\n\n\(error.localizedDescription)"
+            )
             if !isSMB {
-                // Use MainActor to handle broadcaster notification
                 let broadcaster = self.broadcaster
                 Task { @MainActor in
                     broadcaster?.notify(BolusFailureObserver.self, on: .main) {
@@ -620,7 +652,12 @@ final class BaseAPSManager: APSManager, Injectable {
             callback?(true, String(localized: "Bolus cancelled successfully.", comment: "Success message for canceling a bolus"))
         } catch {
             debug(.apsManager, "Bolus cancellation failed with error: \(error)")
-            processError(APSError.pumpError(error))
+            lastError.send(APSError.pumpError(error))
+            issueAlertForCategory(
+                .bolusFailed,
+                title: String(localized: "Bolus cancellation failed"),
+                body: String(localized: "Try again.") + "\n\n\(error.localizedDescription)"
+            )
             callback?(
                 false,
                 String(
@@ -709,10 +746,9 @@ final class BaseAPSManager: APSManager, Injectable {
             throw APSError.apsError(message: "Pump not set")
         }
 
-        // Check if pump is suspended and abort if it is
         if pump.status.pumpStatus.suspended {
-            info(.apsManager, "Skipping enactDetermination because pump is suspended")
-            return // return without throwing an error
+            debug(.apsManager, "Skipping enactDetermination because pump is suspended")
+            return
         }
 
         // Unable to do temp basal during manual temp basal 😁
@@ -1050,17 +1086,14 @@ final class BaseAPSManager: APSManager, Injectable {
             let glucose24h = try await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
             let glucoseOneWeek = try await fetchGlucose(
                 predicate: NSPredicate.predicateForOneWeek,
-                fetchLimit: 288 * 7,
                 batchSize: 250
             )
             let glucoseOneMonth = try await fetchGlucose(
                 predicate: NSPredicate.predicateForOneMonth,
-                fetchLimit: 288 * 7 * 30,
                 batchSize: 500
             )
             let glucoseThreeMonths = try await fetchGlucose(
                 predicate: NSPredicate.predicateForThreeMonths,
-                fetchLimit: 288 * 7 * 30 * 3,
                 batchSize: 1000
             )
 
@@ -1204,9 +1237,108 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
+    private var transientCategoryFirstSeen: [String: Date] = [:]
+    private var transientCategoryCount: [String: Int] = [:]
+    private static let transientDwellThreshold: TimeInterval = 60
+    private static let transientCountThreshold = 2
+
+    /// Set by `markNextLoopUserInitiated()` (e.g. force-loop button), consumed
+    /// on the next entry into `loop()` so that errors during a user-initiated
+    /// loop surface immediately instead of being suppressed by dwell logic.
+    @SyncAccess private var nextLoopUserInitiated: Bool = false
+    private var currentLoopUserInitiated: Bool = false
+
+    func markNextLoopUserInitiated() {
+        nextLoopUserInitiated = true
+    }
+
     private func processError(_ error: Error) {
         warning(.apsManager, "\(error)")
         lastError.send(error)
+        surfaceErrorIfNeeded(error)
+    }
+
+    private func surfaceErrorIfNeeded(_ error: Error) {
+        let category = TrioAlertClassifier.categorize(error: error)
+        let key = String(describing: category)
+
+        if category.shouldFireImmediately || currentLoopUserInitiated {
+            transientCategoryFirstSeen.removeValue(forKey: key)
+            transientCategoryCount.removeValue(forKey: key)
+            issueAlertForError(error, category: category)
+            return
+        }
+
+        let now = Date()
+        let firstSeen = transientCategoryFirstSeen[key] ?? now
+        let count = (transientCategoryCount[key] ?? 0) + 1
+        let dwellElapsed = now.timeIntervalSince(firstSeen)
+        let dwellMet = dwellElapsed >= Self.transientDwellThreshold
+        let countMet = count >= Self.transientCountThreshold
+
+        if dwellMet || countMet {
+            transientCategoryFirstSeen.removeValue(forKey: key)
+            transientCategoryCount.removeValue(forKey: key)
+            issueAlertForError(error, category: category)
+        } else {
+            transientCategoryFirstSeen[key] = firstSeen
+            transientCategoryCount[key] = count
+            debug(
+                .apsManager,
+                "APSManager suppressed transient \(category) (count=\(count)/\(Self.transientCountThreshold), dwell=\(Int(dwellElapsed))s/\(Int(Self.transientDwellThreshold))s)"
+            )
+        }
+    }
+
+    private func issueAlertForCategory(_ category: TrioAlertCategory, title: String, body: String) {
+        let content = Alert.Content(
+            title: title,
+            body: body,
+            acknowledgeActionButtonLabel: String(localized: "OK")
+        )
+        let alert = Alert(
+            identifier: Alert.Identifier(managerIdentifier: "trio.aps", alertIdentifier: category.alertIdentifier),
+            foregroundContent: content,
+            backgroundContent: content,
+            trigger: .immediate,
+            interruptionLevel: category.interruptionLevel
+        )
+        trioAlertManager?.issueAlert(alert)
+    }
+
+    private func issueAlertForError(_ error: Error, category: TrioAlertCategory) {
+        let (title, body) = describeForAlert(error)
+        let content = Alert.Content(
+            title: title,
+            body: body,
+            acknowledgeActionButtonLabel: "OK"
+        )
+        let alert = Alert(
+            identifier: Alert.Identifier(managerIdentifier: "trio.aps", alertIdentifier: category.alertIdentifier),
+            foregroundContent: content,
+            backgroundContent: content,
+            trigger: .immediate,
+            interruptionLevel: category.interruptionLevel
+        )
+        trioAlertManager?.issueAlert(alert)
+    }
+
+    private func describeForAlert(_ error: Error) -> (title: String, body: String) {
+        if let apsError = error as? APSError {
+            switch apsError {
+            case let .pumpError(inner):
+                return (
+                    String(localized: "Pump Error"),
+                    String(localized: "Trio could not communicate with the pump. Check the pump and try again.")
+                        + "\n\n\(inner.localizedDescription)"
+                )
+            case let .invalidPumpState(message): return (String(localized: "Pump State Error"), message)
+            case let .glucoseError(message): return (String(localized: "Glucose Error"), message)
+            case let .apsError(message): return (String(localized: "Algorithm Error"), message)
+            case let .manualBasalTemp(message): return (String(localized: "Manual Temp Basal Active"), message)
+            }
+        }
+        return ("Trio", error.localizedDescription)
     }
 
     /// Called from the `bolusTrigger` Combine sink (already on

+ 32 - 8
Trio/Sources/APS/CGM/PluginSource.swift

@@ -99,23 +99,47 @@ extension PluginSource: CGMManagerDelegate {
         debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
     }
 
-    func issueAlert(_: LoopKit.Alert) {}
+    /// Forwards CGMManager-issued alerts into the unified `TrioAlertManager`
+    /// pipeline so they get the same in-app banner + UN scheduling + history
+    /// logging treatment as everything else. Used to be a no-op; on
+    /// dev-libre3 / LibreLoop builds that meant CGM-issued alerts (sensor
+    /// failure, signal loss, expiry, etc.) silently dropped.
+    func issueAlert(_ alert: LoopKit.Alert) {
+        glucoseManager?.trioAlertManager?.issueAlert(alert)
+    }
 
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
+    func retractAlert(identifier: LoopKit.Alert.Identifier) {
+        glucoseManager?.trioAlertManager?.retractAlert(identifier: identifier)
+    }
 
-    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
+    /// LoopKit asks this on reconnect to avoid re-issuing an alert that's
+    /// still live. `TrioAlertManager` deduplicates downstream via its own
+    /// throttler and live-alert table, so answering "no" here is safe — at
+    /// worst we get one duplicate banner, which the throttler suppresses.
+    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion: @escaping (Result<Bool, Error>) -> Void) {
+        completion(.success(false))
+    }
 
     func lookupAllUnretracted(
         managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
+        completion: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
+    ) {
+        completion(.success([]))
+    }
 
     func lookupAllUnacknowledgedUnretracted(
         managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
+        completion: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
+    ) {
+        completion(.success([]))
+    }
 
-    func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
+    /// LoopKit calls this when a manager itself decides an alert is no
+    /// longer relevant. Mirror the action on our pipeline so the in-app
+    /// banner, scheduled UN, and history all clear.
+    func recordRetractedAlert(_ alert: LoopKit.Alert, at _: Date) {
+        glucoseManager?.trioAlertManager?.retractAlert(identifier: alert.identifier)
+    }
 
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
         processQueue.async { [weak self] in

+ 11 - 45
Trio/Sources/APS/DeviceDataManager.swift

@@ -62,6 +62,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var bluetoothProvider: BluetoothStateManager!
+    @Injected() private var trioAlertManager: TrioAlertManager!
 
     @Persisted(key: "BaseDeviceDataManager.lastEventDate") var lastEventDate: Date? = nil
     @SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date =
@@ -83,10 +84,16 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
     var pumpManager: PumpManagerUI? {
         didSet {
+            if let oldValue = oldValue {
+                trioAlertManager?.unregister(managerIdentifier: oldValue.pluginIdentifier)
+            }
             if let pumpManager = pumpManager {
                 pumpManager.pumpManagerDelegate = self
                 pumpManager.delegateQueue = processQueue
 
+                trioAlertManager?.register(responder: pumpManager, for: pumpManager.pluginIdentifier)
+                trioAlertManager?.register(soundVendor: pumpManager, for: pumpManager.pluginIdentifier)
+
                 /// Since the pump manager has been successfully instantiated from its saved state,
                 /// copy its rawValue to rawPumpManager which will be saved to persistant storage.
                 rawPumpManager = pumpManager.rawValue
@@ -241,7 +248,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         injectServices(resolver)
         setupPumpManager()
         UIDevice.current.isBatteryMonitoringEnabled = true
-        broadcaster.register(AlertObserver.self, observer: self)
     }
 
     func setupPumpManager() {
@@ -633,22 +639,13 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
 extension BaseDeviceDataManager: DeviceManagerDelegate {
     func issueAlert(_ alert: Alert) {
-        alertHistoryStorage.addAlert(
-            AlertEntry(
-                alertIdentifier: alert.identifier.alertIdentifier,
-                primitiveInterruptionLevel: alert.interruptionLevel.storedValue as? Decimal,
-                issuedDate: Date(),
-                managerIdentifier: alert.identifier.managerIdentifier,
-                triggerType: alert.trigger.storedType,
-                triggerInterval: alert.trigger.storedInterval as? Decimal,
-                contentTitle: alert.foregroundContent?.title,
-                contentBody: alert.foregroundContent?.body
-            )
-        )
+        debug(.deviceManager, "issueAlert \(alert.identifier.value)")
+        trioAlertManager.issueAlert(alert)
     }
 
     func retractAlert(identifier: Alert.Identifier) {
-        alertHistoryStorage.removeAlert(identifier: identifier.alertIdentifier)
+        debug(.deviceManager, "retractAlert \(identifier.value)")
+        trioAlertManager.retractAlert(identifier: identifier)
     }
 
     func doesIssuedAlertExist(identifier _: Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {
@@ -703,37 +700,6 @@ extension BaseDeviceDataManager: CGMManagerDelegate {
     func cgmManager(_: CGMManager, didUpdate _: CGMManagerStatus) {}
 }
 
-// MARK: - AlertPresenter
-
-extension BaseDeviceDataManager: AlertObserver {
-    func AlertDidUpdate(_ alerts: [AlertEntry]) {
-        alerts.forEach { alert in
-            if alert.acknowledgedDate == nil {
-                ackAlert(alert: alert)
-            }
-        }
-    }
-
-    private func ackAlert(alert: AlertEntry) {
-        let alertIssueDate = alert.issuedDate
-
-        processQueue.async {
-            self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in
-                if let error = error {
-                    self.alertHistoryStorage.acknowledgeAlert(alertIssueDate, error.localizedDescription)
-                    debug(.deviceManager, "acknowledge not succeeded with error \(error)")
-                } else {
-                    self.alertHistoryStorage.acknowledgeAlert(alertIssueDate, nil)
-                }
-            }
-
-            self.broadcaster.notify(pumpNotificationObserver.self, on: self.processQueue) {
-                $0.pumpNotification(alert: alert)
-            }
-        }
-    }
-}
-
 // extension BaseDeviceDataManager: AlertPresenter {
 //    func issueAlert(_: Alert) {}
 //    func retractAlert(identifier _: Alert.Identifier) {}

+ 5 - 0
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -21,6 +21,10 @@ protocol FetchGlucoseManager: SourceInfoProvider {
     var shouldSyncToRemoteService: Bool { get }
     var cgmDisplayState: CurrentValueSubject<CgmDisplayState?, Never> { get }
     var cgmProgressHighlight: CurrentValueSubject<DeviceLifecycleProgress?, Never> { get }
+    /// Routes CGMManager-issued alerts (sensor failure, signal loss, expiry,
+    /// etc.) into the unified `TrioAlertManager` pipeline. Read by
+    /// `PluginSource.issueAlert` / `retractAlert`.
+    var trioAlertManager: TrioAlertManager! { get }
 }
 
 extension FetchGlucoseManager {
@@ -42,6 +46,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var deviceDataManager: DeviceDataManager!
     @Injected() var pluginCGMManager: PluginManager!
     @Injected() var calibrationService: CalibrationService!
+    @Injected() var trioAlertManager: TrioAlertManager!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)

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

@@ -214,6 +214,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
         guard context.hasChanges else { return }
         try context.save()
+        updateSubject.send()
     }
 
     private func storeGlucoseBatch(_ glucose: [BloodGlucose]) throws {
@@ -232,7 +233,6 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             }
         )
         try context.execute(batchInsert)
-        // Only send update for batch insert since regular save triggers CoreData notifications
         updateSubject.send()
     }
 
@@ -771,11 +771,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
                 let glucoseValue = glucose.glucose
 
-                if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
+                if Decimal(glucoseValue) <= settingsManager.settings.low {
                     return .low
                 }
 
-                if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
+                if Decimal(glucoseValue) >= settingsManager.settings.high {
                     return .high
                 }
 

+ 1 - 2
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -274,8 +274,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             onContext: context,
             predicate: NSPredicate.pumpHistoryLast24h,
             key: "timestamp",
-            ascending: false,
-            fetchLimit: 288
+            ascending: false
         )
 
         return await context.perform {

+ 3 - 0
Trio/Sources/Application/TrioApp.swift

@@ -90,6 +90,9 @@ extension Notification.Name {
             _ = resolver.resolve(LiveActivityManager.self)!
         }
         _ = resolver.resolve(IOBService.self)!
+        _ = resolver.resolve(GlucoseAlertCoordinator.self)!
+        _ = resolver.resolve(NotLoopingMonitor.self)!
+        _ = DeviceAlertsStore.shared
     }
 
     init() {

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

@@ -17,6 +17,12 @@ final class ServiceAssembly: Assembly {
         container.register(CalendarManager.self) { r in BaseCalendarManager(resolver: r) }
         container.register(HKHealthStore.self) { _ in HKHealthStore() }
         container.register(HealthKitManager.self) { r in BaseHealthKitManager(resolver: r) }
+        container.register(TrioAlertManager.self) { r in BaseTrioAlertManager(resolver: r) }
+            .inObjectScope(.container)
+        container.register(GlucoseAlertCoordinator.self) { r in GlucoseAlertCoordinator(resolver: r) }
+            .inObjectScope(.container)
+        container.register(NotLoopingMonitor.self) { r in NotLoopingMonitor(resolver: r) }
+            .inObjectScope(.container)
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(BolusCalculationManager.self) { r in BaseBolusCalculationManager(resolver: r) }

+ 29 - 0
Trio/Sources/Helpers/TimeInterval+Convenience.swift

@@ -0,0 +1,29 @@
+import Foundation
+
+/// Convenience constructors + getters used across the app for `TimeInterval`
+/// math at minute/hour granularity. Previously lived inside the deleted
+/// `Snooze` module; broken out here because real callers exist outside the
+/// alert pipeline (chart markers, pump-history windows, fetch timers, etc.).
+extension TimeInterval {
+    static func seconds(_ seconds: Double) -> TimeInterval { seconds }
+
+    static func minutes(_ minutes: Double) -> TimeInterval {
+        TimeInterval(minutes: minutes)
+    }
+
+    static func hours(_ hours: Double) -> TimeInterval {
+        TimeInterval(minutes: hours * 60)
+    }
+
+    init(minutes: Double) {
+        self.init(minutes * 60)
+    }
+
+    init(hours: Double) {
+        self.init(minutes: hours * 60)
+    }
+
+    var minutes: Double { self / 60.0 }
+
+    var hours: Double { minutes / 60.0 }
+}

File diff suppressed because it is too large
+ 542 - 4
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 1 - 47
Trio/Sources/Logger/Logger.swift

@@ -5,7 +5,6 @@ import UIKit
 var LoggerTestMode = false
 
 private let baseReporter = TrioApp.resolver.resolve(GroupedIssueReporter.self)!
-private let router = TrioApp.resolver.resolve(Router.self)!
 
 let loggerLock = NSRecursiveLock()
 
@@ -28,37 +27,13 @@ func debug(
 func info(
     _ category: Logger.Category,
     _ message: String,
-    type: MessageType = .info,
     file: String = #file,
     function: String = #function,
     line: UInt = #line
 ) {
     DispatchWorkItem(qos: .background, flags: .enforceQoS) {
         loggerLock.perform {
-            category.logger.info(message, type: type, file: file, function: function, line: line)
-        }
-    }.perform()
-}
-
-func info(
-    _ category: Logger.Category,
-    _ message: String,
-    notificationText: String,
-    type: MessageType = .info,
-    file: String = #file,
-    function: String = #function,
-    line: UInt = #line
-) {
-    DispatchWorkItem(qos: .background, flags: .enforceQoS) {
-        loggerLock.perform {
-            category.logger.info(
-                message,
-                notificationText: notificationText,
-                type: type,
-                file: file,
-                function: function,
-                line: line
-            )
+            category.logger.info(message, file: file, function: function, line: line)
         }
     }.perform()
 }
@@ -268,18 +243,6 @@ final class Logger {
 
     func info(
         _ message: String,
-        type: MessageType = .info,
-        file: String = #file,
-        function: String = #function,
-        line: UInt = #line
-    ) {
-        info(message, notificationText: message, type: type, file: file, function: function, line: line)
-    }
-
-    func info(
-        _ message: String,
-        notificationText: String,
-        type: MessageType = .info,
         file: String = #file,
         function: String = #function,
         line: UInt = #line
@@ -287,8 +250,6 @@ final class Logger {
         let printedMessage = "INFO: \(message)"
         os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, printedMessage)
         reporter.log(category.name, printedMessage, file: file, function: function, line: line)
-
-        showAlert(notificationText, type: type)
     }
 
     func warning(
@@ -324,13 +285,6 @@ final class Logger {
         )
     }
 
-    private func showAlert(_ message: String, type: MessageType = .info) {
-        DispatchQueue.main.async {
-            let messageCont = MessageContent(content: message, type: type)
-            router.alertMessage.send(messageCont)
-        }
-    }
-
     fileprivate func errorWithoutFatalError(
         _ message: String,
         description: String? = nil,

+ 0 - 2
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -33,8 +33,6 @@ class PickerSettingsProvider: ObservableObject {
 }
 
 struct DecimalPickerSettings {
-    var lowGlucose = PickerSetting(value: 70, step: 5, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
-    var highGlucose = PickerSetting(value: 180, step: 5, min: 100, max: 400, type: PickerSetting.PickerSettingType.glucose)
     var carbsRequiredThreshold = PickerSetting(value: 10, step: 1, min: 0, max: 100, type: PickerSetting.PickerSettingType.gram)
     var individualAdjustmentFactor = PickerSetting(
         value: 0.5,

+ 34 - 0
Trio/Sources/Models/GlucoseAlerts/AlarmSoundCatalog.swift

@@ -0,0 +1,34 @@
+import Foundation
+
+/// Catalog of the bundled critical-alarm sound files (`Trio/Resources/Sounds/`).
+/// Ported from Loop's audio-critical-alerts branch. Single source of truth
+/// for the alarm sound picker.
+enum AlarmSoundCatalog {
+    /// (filename, displayName) tuples in display order.
+    private static let catalog: [(filename: String, displayName: String)] = [
+        ("urgent_low.caf", String(localized: "Urgent Low")),
+        ("critical.caf", String(localized: "Critical")),
+        ("alarm.caf", String(localized: "Alarm")),
+        ("bright_alarm.caf", String(localized: "Bright Alarm")),
+        ("honk.caf", String(localized: "Honk")),
+        ("trill.caf", String(localized: "Trill")),
+        ("chime.caf", String(localized: "Chime")),
+        ("clear_chimes.caf", String(localized: "Clear Chimes")),
+        ("high_chimes.caf", String(localized: "High Chimes")),
+        ("dings.caf", String(localized: "Dings")),
+        ("bloom.caf", String(localized: "Bloom")),
+        ("bloop.caf", String(localized: "Bloop")),
+        ("spring.caf", String(localized: "Spring")),
+        ("minimal.caf", String(localized: "Minimal")),
+        ("simple.caf", String(localized: "Simple")),
+        ("synth.caf", String(localized: "Synth")),
+        ("mood_synth.caf", String(localized: "Mood Synth")),
+        ("crying.caf", String(localized: "Crying"))
+    ]
+
+    static let allFilenames: [String] = catalog.map(\.filename)
+
+    static func displayName(for filename: String) -> String {
+        catalog.first { $0.filename == filename }?.displayName ?? filename
+    }
+}

+ 33 - 0
Trio/Sources/Models/GlucoseAlerts/DayNightOptions.swift

@@ -0,0 +1,33 @@
+import Foundation
+
+protocol DayNightDisplayable {
+    var displayName: String { get }
+}
+
+extension DayNightDisplayable where Self: RawRepresentable, Self.RawValue == String {
+    var displayName: String {
+        rawValue == "always"
+            ? String(localized: "Day & Night")
+            : rawValue.localizedCapitalized
+    }
+}
+
+enum ActiveOption: String, CaseIterable, Codable, Identifiable, DayNightDisplayable {
+    case always
+    case day
+    case night
+
+    var id: String { rawValue }
+}
+
+struct TimeOfDay: Codable, Equatable, Hashable {
+    var hour: Int
+    var minute: Int
+
+    init(hour: Int, minute: Int) {
+        self.hour = max(0, min(23, hour))
+        self.minute = max(0, min(59, minute))
+    }
+
+    var minutesSinceMidnight: Int { hour * 60 + minute }
+}

+ 79 - 0
Trio/Sources/Models/GlucoseAlerts/DeviceAlertSeverity.swift

@@ -0,0 +1,79 @@
+import Foundation
+import LoopKit
+
+/// Coarse-grained tier the user configures for device alarms. Wraps
+/// `Alert.InterruptionLevel` plus its visible behavior:
+/// - `.critical` overrides Silence & Focus Mode (uses the critical-audio
+///   fallback for builds without the Critical Alerts entitlement)
+/// - `.timeSensitive` pierces normal banner suppression but obeys DND/silent
+/// - `.normal` fires only when the device isn't silenced — informational
+enum DeviceAlertSeverity: String, Codable, CaseIterable, Identifiable {
+    case critical
+    case timeSensitive
+    case normal
+
+    var id: String { rawValue }
+
+    init?(level: Alert.InterruptionLevel) {
+        switch level {
+        case .critical: self = .critical
+        case .timeSensitive: self = .timeSensitive
+        case .active: self = .normal
+        }
+    }
+
+    var displayName: String {
+        switch self {
+        case .critical: return String(localized: "Critical")
+        case .timeSensitive: return String(localized: "Time-Sensitive")
+        case .normal: return String(localized: "Normal")
+        }
+    }
+
+    var blurb: String {
+        switch self {
+        case .critical:
+            return String(localized: "Overrides Silence & Focus Mode. For situations requiring immediate attention.")
+        case .timeSensitive:
+            return String(localized: "Pierces banner suppression but obeys Silence & Focus Mode by default.")
+        case .normal:
+            return String(localized: "Default notification banner. Suppressed by Silence & Focus Mode by default.")
+        }
+    }
+
+    var hintText: String {
+        switch self {
+        case .critical:
+            return String(
+                localized: "For situations that require prompt attention. These break through Silent Mode, Do Not Disturb, and any Focus you have enabled. Examples: a pump fault, an occlusion, or Trio not looping for too long. Heads up: if your build of Trio has Apple's Critical Alerts entitlement, iOS plays its own critical alert sound and the sound you picked for this alert category is ignored."
+            )
+        case .timeSensitive:
+            return String(
+                localized: "For things you should know about soon, but not 'act right now'. These can break through banner suppression on the lock screen, but they still obey Silent Mode and Focus by default. Examples: reservoir running low, pod or patch expiring soon, or glucose data going stale."
+            )
+        case .normal:
+            return String(
+                localized: "For everyday heads-up notifications. These behave like a standard banner — they stay quiet when your phone is silenced or a Focus is on. Examples: an algorithm error, a sensor expiration reminder, or a time-zone change being detected."
+            )
+        }
+    }
+
+    var defaultSoundFilename: String {
+        switch self {
+        case .critical: return "alarm.caf"
+        case .timeSensitive: return "chime.caf"
+        case .normal: return "bloop.caf"
+        }
+    }
+
+    /// Default for the per-tier override toggle when seeded. Tier names are
+    /// labels now — the actual `Alert.InterruptionLevel` is derived from the
+    /// override flag at fire time (true → `.critical`, false → `.timeSensitive`).
+    var defaultOverridesSilenceAndDND: Bool {
+        switch self {
+        case .critical: return true
+        case .normal,
+             .timeSensitive: return false
+        }
+    }
+}

+ 63 - 0
Trio/Sources/Models/GlucoseAlerts/DeviceAlertSeverityConfig.swift

@@ -0,0 +1,63 @@
+import Foundation
+
+/// One user-configured behavior variant for a device-alarm severity tier.
+/// Multiple configs per severity are allowed — each with its own
+/// `activeOption` (Day & Night / Day only / Night only) — so the user can
+/// e.g. have a Critical config that overrides Silence during the day and
+/// a second Critical config that goes silent at night.
+///
+/// Lookup at fire time picks the variant whose `activeOption` matches the
+/// current day/night window, falling back to the `.always` variant.
+struct DeviceAlertSeverityConfig: Codable, Equatable, Identifiable {
+    var id: UUID
+    var severity: DeviceAlertSeverity
+    var isEnabled: Bool
+    var soundFilename: String
+    var playsSound: Bool
+    /// When true, alarms in this tier bypass Focus Mode / silent switch
+    /// (maps to `Alert.InterruptionLevel.critical` and engages the in-process
+    /// `CriticalAlertAudioPlayer` fallback if `playsSound` is true).
+    /// When false, the alarm uses `.timeSensitive`.
+    var overridesSilenceAndDND: Bool
+    var activeOption: ActiveOption
+
+    init(
+        id: UUID = UUID(),
+        severity: DeviceAlertSeverity,
+        activeOption: ActiveOption = .always
+    ) {
+        self.id = id
+        self.severity = severity
+        isEnabled = true
+        soundFilename = severity.defaultSoundFilename
+        playsSound = true
+        overridesSilenceAndDND = severity.defaultOverridesSilenceAndDND
+        self.activeOption = activeOption
+    }
+
+    // MARK: - Codable
+
+    private enum CodingKeys: String, CodingKey {
+        case id
+        case severity
+        case isEnabled
+        case soundFilename
+        case playsSound
+        case overridesSilenceAndDND
+        case activeOption
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
+        severity = try container.decode(DeviceAlertSeverity.self, forKey: .severity)
+        isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
+        soundFilename = try container.decodeIfPresent(String.self, forKey: .soundFilename) ?? severity.defaultSoundFilename
+        playsSound = try container.decodeIfPresent(Bool.self, forKey: .playsSound) ?? true
+        overridesSilenceAndDND = try container.decodeIfPresent(
+            Bool.self,
+            forKey: .overridesSilenceAndDND
+        ) ?? severity.defaultOverridesSilenceAndDND
+        activeOption = try container.decodeIfPresent(ActiveOption.self, forKey: .activeOption) ?? .always
+    }
+}

+ 77 - 0
Trio/Sources/Models/GlucoseAlerts/GlucoseAlert.swift

@@ -0,0 +1,77 @@
+import Foundation
+
+/// A single configurable glucose alarm. Multiple entries of the same type are
+/// allowed — e.g. a Low alarm `active: .day` at 80 mg/dL plus a second Low
+/// `active: .night` at 70 mg/dL.
+struct GlucoseAlert: Identifiable, Codable, Equatable {
+    var id: UUID
+    var type: GlucoseAlertType
+    var name: String
+    var isEnabled: Bool
+    var thresholdMgDL: Decimal
+    var soundFilename: String
+    /// When false, the alarm fires the banner / notification but no sound.
+    /// iOS still drives haptics from the interruption level.
+    var playsSound: Bool
+    /// When true, this alarm bypasses Focus Mode / silent switch
+    /// modes. Maps to `Alert.InterruptionLevel.critical` and triggers the
+    /// in-process `CriticalAlertAudioPlayer` fallback for builds without the
+    /// Critical Alerts entitlement.
+    var overridesSilenceAndDND: Bool
+    var activeOption: ActiveOption
+    /// Per-alarm snooze. Distinct from the global mute on `AlertMuter`.
+    var snoozedUntil: Date?
+
+    init(type: GlucoseAlertType) {
+        id = UUID()
+        self.type = type
+        name = type.displayName
+        isEnabled = true
+        thresholdMgDL = type.defaultThresholdMgDL
+        soundFilename = type.defaultSoundFilename
+        playsSound = true
+        overridesSilenceAndDND = type.defaultOverridesSilenceAndDND
+        activeOption = .always
+        snoozedUntil = nil
+    }
+
+    /// Whether the coordinator should fire this alarm when a reading breaches.
+    /// Urgent-low is the safety floor — the editor hides the Enabled toggle so
+    /// the user can't accidentally turn it off, and stored `isEnabled = false`
+    /// from a prior install is ignored here.
+    var shouldEvaluate: Bool {
+        type == .urgentLow || isEnabled
+    }
+
+    // MARK: - Codable
+
+    private enum CodingKeys: String, CodingKey {
+        case id
+        case type
+        case name
+        case isEnabled
+        case thresholdMgDL
+        case soundFilename
+        case playsSound
+        case overridesSilenceAndDND
+        case activeOption
+        case snoozedUntil
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        id = try container.decode(UUID.self, forKey: .id)
+        type = try container.decode(GlucoseAlertType.self, forKey: .type)
+        name = try container.decode(String.self, forKey: .name)
+        isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
+        thresholdMgDL = try container.decode(Decimal.self, forKey: .thresholdMgDL)
+        soundFilename = try container.decodeIfPresent(String.self, forKey: .soundFilename) ?? type.defaultSoundFilename
+        playsSound = try container.decodeIfPresent(Bool.self, forKey: .playsSound) ?? true
+        overridesSilenceAndDND = try container.decodeIfPresent(
+            Bool.self,
+            forKey: .overridesSilenceAndDND
+        ) ?? type.defaultOverridesSilenceAndDND
+        activeOption = try container.decodeIfPresent(ActiveOption.self, forKey: .activeOption) ?? .always
+        snoozedUntil = try container.decodeIfPresent(Date.self, forKey: .snoozedUntil)
+    }
+}

+ 63 - 0
Trio/Sources/Models/GlucoseAlerts/GlucoseAlertConfiguration.swift

@@ -0,0 +1,63 @@
+import Foundation
+
+struct GlucoseAlertConfiguration: Codable, Equatable {
+    var dayStart: TimeOfDay
+    var nightStart: TimeOfDay
+    /// Force Trio alarms on even when the CGM advertises its own.
+    var forceTrioAlertsWhenCGMProvidesOwn: Bool
+
+    init(
+        dayStart: TimeOfDay = TimeOfDay(hour: 6, minute: 0),
+        nightStart: TimeOfDay = TimeOfDay(hour: 22, minute: 0),
+        forceTrioAlertsWhenCGMProvidesOwn: Bool = false
+    ) {
+        self.dayStart = dayStart
+        self.nightStart = nightStart
+        self.forceTrioAlertsWhenCGMProvidesOwn = forceTrioAlertsWhenCGMProvidesOwn
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case dayStart
+        case nightStart
+        case forceTrioAlertsWhenCGMProvidesOwn
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        dayStart = try container.decode(TimeOfDay.self, forKey: .dayStart)
+        nightStart = try container.decode(TimeOfDay.self, forKey: .nightStart)
+        forceTrioAlertsWhenCGMProvidesOwn = try container.decodeIfPresent(
+            Bool.self,
+            forKey: .forceTrioAlertsWhenCGMProvidesOwn
+        ) ?? false
+    }
+
+    /// Resolve whether `date` falls into the user's "night" window. Mirrors
+    /// LoopFollow's logic: handles both same-day (06→22) and wrap-around
+    /// (22→06) ranges. When `nightStart >= dayStart`, night is "later than
+    /// nightStart OR earlier than dayStart"; otherwise night is the slice
+    /// between nightStart and dayStart.
+    func isNight(at date: Date, calendar: Calendar = .current) -> Bool {
+        let startOfDay = calendar.startOfDay(for: date)
+        guard
+            let dayStartDate = calendar.date(
+                bySettingHour: dayStart.hour,
+                minute: dayStart.minute,
+                second: 0,
+                of: startOfDay
+            ),
+            let nightStartDate = calendar.date(
+                bySettingHour: nightStart.hour,
+                minute: nightStart.minute,
+                second: 0,
+                of: startOfDay
+            )
+        else { return false }
+
+        if nightStartDate >= dayStartDate {
+            return date >= nightStartDate || date < dayStartDate
+        } else {
+            return date >= nightStartDate && date < dayStartDate
+        }
+    }
+}

+ 103 - 0
Trio/Sources/Models/GlucoseAlerts/GlucoseAlertType.swift

@@ -0,0 +1,103 @@
+import Foundation
+import LoopKit
+
+/// Glucose-driven alarm types Trio supports today. Order is priority order
+/// (`urgentLow` highest), matching how `AlarmType.priority` works in
+/// LoopFollow — when multiple alarms would fire on the same reading, the
+/// higher-priority one wins and lower types of the same group are skipped.
+enum GlucoseAlertType: String, Codable, CaseIterable, Identifiable {
+    case urgentLow
+    case low
+    case forecastedLow
+    case high
+    /// Driven by `Determination.carbsReq`, not by a glucose reading. Stored
+    /// alongside the other glucose alarms so the user has one place to
+    /// configure schedule/sound/snooze for everything fired by Trio.
+    case carbsRequired
+
+    var id: String { rawValue }
+
+    var priority: Int { Self.allCases.firstIndex(of: self) ?? 0 }
+
+    /// `true` when the alarm fires off a CGM glucose reading. `false` for
+    /// `forecastedLow` (driven by the determination forecast) and
+    /// `carbsRequired` (driven by the determination's `carbsReq` field).
+    var isReadingDriven: Bool {
+        switch self {
+        case .high,
+             .low,
+             .urgentLow:
+            return true
+        case .carbsRequired,
+             .forecastedLow:
+            return false
+        }
+    }
+
+    /// Parses a glucose-alarm slug emitted by `GlucoseAlertCoordinator`
+    /// (`glucose.<type>.<uuid>`). Returns nil for non-glucose alert
+    /// identifiers — used by `BaseTrioAlertManager.requestSnooze` to decide
+    /// between per-type and global mute routing.
+    init?(slug: String) {
+        let parts = slug.split(separator: ".")
+        guard parts.count >= 2, parts[0] == "glucose" else { return nil }
+        guard let parsed = GlucoseAlertType(rawValue: String(parts[1])) else { return nil }
+        self = parsed
+    }
+
+    var displayName: String {
+        switch self {
+        case .urgentLow: return String(localized: "Urgent Low Glucose")
+        case .low: return String(localized: "Low Glucose")
+        case .forecastedLow: return String(localized: "Low Glucose Soon")
+        case .high: return String(localized: "High Glucose")
+        case .carbsRequired: return String(localized: "Carbs Required")
+        }
+    }
+
+    var blurb: String {
+        switch self {
+        case .urgentLow: return String(localized: "Fires when glucose drops to or below an urgent low threshold.")
+        case .low: return String(localized: "Fires when glucose drops to or below a low threshold.")
+        case .forecastedLow: return String(localized: "Fires when glucose is forecasted to be low within the next 20 minutes.")
+        case .high: return String(localized: "Fires when glucose rises to or above a high threshold.")
+        case .carbsRequired: return String(localized: "Fires when oref recommends eating carbs to avoid a low.")
+        }
+    }
+
+    /// Default threshold when adding a new alarm. Mg/dL for glucose types,
+    /// grams for `carbsRequired`.
+    var defaultThresholdMgDL: Decimal {
+        switch self {
+        case .urgentLow: return 54
+        case .low: return 72
+        case .forecastedLow: return 72
+        case .high: return 270
+        case .carbsRequired: return 10
+        }
+    }
+
+    /// Default bundled sound filename. See `Trio/Resources/Sounds/`.
+    var defaultSoundFilename: String {
+        switch self {
+        case .urgentLow: return "urgent_low.caf"
+        case .low: return "trill.caf"
+        case .forecastedLow: return "bloom.caf"
+        case .high: return "chime.caf"
+        case .carbsRequired: return "bloop.caf"
+        }
+    }
+
+    /// Default for `GlucoseAlert.overridesSilenceAndDND` when adding a new
+    /// alarm. Urgent-low defaults to override-on to match Loop's stance;
+    /// others default off (time-sensitive, doesn't pierce DND / Focus).
+    var defaultOverridesSilenceAndDND: Bool {
+        switch self {
+        case .urgentLow: return true
+        case .carbsRequired,
+             .forecastedLow,
+             .high,
+             .low: return false
+        }
+    }
+}

+ 0 - 27
Trio/Sources/Models/GlucoseNotificationsOption.swift

@@ -1,27 +0,0 @@
-//
-//  GlucoseNotificationOption.swift
-//  FreeAPS
-//
-//  Created by Kimberlie Skandis on 1/18/25.
-//
-import Foundation
-import SwiftUI
-
-public enum GlucoseNotificationsOption: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    case disabled
-    case alwaysEveryCGM
-    case onlyAlarmLimits
-
-    public var id: String { rawValue }
-
-    var displayName: String {
-        switch self {
-        case .disabled:
-            return String(localized: "Disabled", comment: "Option to disable glucose notifications")
-        case .alwaysEveryCGM:
-            return String(localized: "Always", comment: "Option to always notify on every CGM reading")
-        case .onlyAlarmLimits:
-            return String(localized: "Only Alarm Limits", comment: "Option to notify only when glucose reaches alarm limits")
-        }
-    }
-}

+ 10 - 0
Trio/Sources/Models/GlucoseSourceKey.swift

@@ -0,0 +1,10 @@
+import Foundation
+
+/// Keys used by `GlucoseSource.sourceInfo()` implementations to advertise
+/// metadata about the active CGM (description, transmitter battery,
+/// Nightscout ping). Consumed by status views + diagnostics.
+enum GlucoseSourceKey: String {
+    case transmitterBattery
+    case nightscoutPing
+    case description
+}

+ 11 - 9
Trio/Sources/Models/NotificationIdentifiers.swift

@@ -6,7 +6,7 @@ enum NotificationCategoryIdentifier: String {
 }
 
 enum NotificationResponseAction: String, CaseIterable {
-    case snooze20 = "Trio.snooze20"
+    case snooze15 = "Trio.snooze15"
     case snooze1hr = "Trio.snooze1hr"
     case snooze3hr = "Trio.snooze3hr"
     case snooze6hr = "Trio.snooze6hr"
@@ -17,8 +17,8 @@ enum NotificationResponseAction: String, CaseIterable {
 
     var minutes: Int {
         switch self {
-        case .snooze20:
-            return 20
+        case .snooze15:
+            return 15
         case .snooze1hr:
             return 60
         case .snooze3hr:
@@ -30,14 +30,14 @@ enum NotificationResponseAction: String, CaseIterable {
 
     var localizedTitle: String {
         switch self {
-        case .snooze20:
-            return String(localized: "20 min", comment: "Snooze glucose alerts for 20 minutes")
+        case .snooze15:
+            return String(localized: "Snooze 15 min", comment: "Snooze glucose alerts for 15 minutes")
         case .snooze1hr:
-            return String(localized: "1 hour", comment: "Snooze glucose alerts for 1 hour")
+            return String(localized: "Snooze 1 hr", comment: "Snooze glucose alerts for 1 hour")
         case .snooze3hr:
-            return String(localized: "3 hours", comment: "Snooze glucose alerts for 3 hours")
+            return String(localized: "Snooze 3 hrs", comment: "Snooze glucose alerts for 3 hours")
         case .snooze6hr:
-            return String(localized: "6 hours", comment: "Snooze glucose alerts for 6 hours")
+            return String(localized: "Snooze 6 hrs", comment: "Snooze glucose alerts for 6 hours")
         }
     }
 }
@@ -54,11 +54,13 @@ enum NotificationCategoryFactory {
             )
         }
 
+        // `.customDismissAction` — otherwise iOS swallows swipes silently and
+        // the in-app banner outlives the notification.
         return UNNotificationCategory(
             identifier: NotificationCategoryIdentifier.trioAlert.rawValue,
             actions: snoozeActions,
             intentIdentifiers: [],
-            options: []
+            options: [.customDismissAction]
         )
     }
 }

+ 0 - 46
Trio/Sources/Models/TrioSettings.swift

@@ -30,14 +30,6 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var displayCalendarIOBandCOB: Bool = false
     var displayCalendarEmojis: Bool = false
     var glucoseBadge: Bool = false
-    var notificationsPump: Bool = true
-    var notificationsCgm: Bool = true
-    var notificationsCarb: Bool = true
-    var notificationsAlgorithm: Bool = true
-    var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
-    var addSourceInfoToGlucoseNotifications: Bool = false
-    var lowGlucose: Decimal = 72
-    var highGlucose: Decimal = 270
     var carbsRequiredThreshold: Decimal = 10
     var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = false
@@ -215,44 +207,6 @@ extension TrioSettings: Decodable {
             settings.delay = delay
         }
 
-        if let notificationsPump = try? container.decode(Bool.self, forKey: .notificationsPump) {
-            settings.notificationsPump = notificationsPump
-        }
-
-        if let notificationsCgm = try? container.decode(Bool.self, forKey: .notificationsCgm) {
-            settings.notificationsCgm = notificationsCgm
-        }
-
-        if let notificationsCarb = try? container.decode(Bool.self, forKey: .notificationsCarb) {
-            settings.notificationsCarb = notificationsCarb
-        }
-
-        if let notificationsAlgorithm = try? container.decode(Bool.self, forKey: .notificationsAlgorithm) {
-            settings.notificationsAlgorithm = notificationsAlgorithm
-        }
-
-        if let glucoseNotificationsOption = try? container.decode(
-            GlucoseNotificationsOption.self,
-            forKey: .glucoseNotificationsOption
-        ) {
-            settings.glucoseNotificationsOption = glucoseNotificationsOption
-        }
-
-        if let addSourceInfoToGlucoseNotifications = try? container.decode(
-            Bool.self,
-            forKey: .addSourceInfoToGlucoseNotifications
-        ) {
-            settings.addSourceInfoToGlucoseNotifications = addSourceInfoToGlucoseNotifications
-        }
-
-        if let lowGlucose = try? container.decode(Decimal.self, forKey: .lowGlucose) {
-            settings.lowGlucose = lowGlucose
-        }
-
-        if let highGlucose = try? container.decode(Decimal.self, forKey: .highGlucose) {
-            settings.highGlucose = highGlucose
-        }
-
         if let carbsRequiredThreshold = try? container.decode(Decimal.self, forKey: .carbsRequiredThreshold) {
             settings.carbsRequiredThreshold = carbsRequiredThreshold
         }

+ 7 - 0
Trio/Sources/Modules/AlarmWindows/AlarmWindowsDataFlow.swift

@@ -0,0 +1,7 @@
+import Combine
+
+enum AlarmWindows {
+    enum Config {}
+}
+
+protocol AlarmWindowsProvider: Provider {}

+ 5 - 0
Trio/Sources/Modules/AlarmWindows/AlarmWindowsProvider.swift

@@ -0,0 +1,5 @@
+import Foundation
+
+extension AlarmWindows {
+    final class Provider: BaseProvider, AlarmWindowsProvider {}
+}

+ 18 - 0
Trio/Sources/Modules/AlarmWindows/AlarmWindowsStateModel.swift

@@ -0,0 +1,18 @@
+import Combine
+import SwiftUI
+
+extension AlarmWindows {
+    final class StateModel: BaseStateModel<Provider> {
+        @Published var units: GlucoseUnits = .mgdL
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+        }
+    }
+}
+
+extension AlarmWindows.StateModel: SettingsObserver {
+    func settingsDidChange(_: TrioSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 135 - 0
Trio/Sources/Modules/AlarmWindows/View/AlarmWindowsRootView.swift

@@ -0,0 +1,135 @@
+import SwiftUI
+import Swinject
+
+extension AlarmWindows {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @StateObject private var store = GlucoseAlertsStore.shared
+
+        @State private var shouldDisplayHint: Bool = false
+        @State private var hintDetent = PresentationDetent.large
+        @State private var selectedVerboseHint: AnyView?
+        @State private var hintLabel: String?
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        private var dayStart: Binding<Date> {
+            Binding(
+                get: { Self.dateFromTimeOfDay(store.configuration.dayStart) },
+                set: { store.configuration.dayStart = Self.timeOfDay(from: $0) }
+            )
+        }
+
+        private var nightStart: Binding<Date> {
+            Binding(
+                get: { Self.dateFromTimeOfDay(store.configuration.nightStart) },
+                set: { store.configuration.nightStart = Self.timeOfDay(from: $0) }
+            )
+        }
+
+        var body: some View {
+            List {
+                Section(header: Text("Window Boundaries")) {
+                    VStack {
+                        DatePicker(
+                            selection: dayStart,
+                            displayedComponents: .hourAndMinute
+                        ) {
+                            HStack {
+                                Image(systemName: "sun.max.fill").foregroundStyle(.orange)
+                                Text("Day Starts")
+                            }
+                        }
+                        .padding(.top)
+
+                        DatePicker(
+                            selection: nightStart,
+                            displayedComponents: .hourAndMinute
+                        ) {
+                            HStack {
+                                Image(systemName: "moon.stars.fill").foregroundStyle(.indigo)
+                                Text("Night Starts")
+                            }
+                        }
+
+                        HStack(alignment: .center) {
+                            Text("Decides when each alarm's Day or Night setting applies.")
+                                .lineLimit(nil)
+                                .font(.footnote)
+                                .foregroundColor(.secondary)
+
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = String(localized: "Day and Night Window")
+                                    let dayDefault = Self.formattedTimeOfDay(hour: 6, minute: 0)
+                                    let nightDefault = Self.formattedTimeOfDay(hour: 22, minute: 0)
+                                    selectedVerboseHint = AnyView(
+                                        VStack(alignment: .leading, spacing: 10) {
+                                            Text(
+                                                String(
+                                                    format: String(localized: "Default: Day starts %1$@, Night starts %2$@."),
+                                                    dayDefault,
+                                                    nightDefault
+                                                )
+                                            ).bold()
+                                            Text(
+                                                "These two times define the Day and Night windows. Each alarm's Active setting picks one — Day & Night, Day only, or Night only — and only fires when that window is current."
+                                            )
+                                            Text(
+                                                String(
+                                                    format: String(
+                                                        localized: "The Night window runs from 'Night Starts' back around to 'Day Starts' — so by default, Night covers %1$@ through %2$@ the next morning."
+                                                    ),
+                                                    nightDefault,
+                                                    dayDefault
+                                                )
+                                            )
+                                            Text(
+                                                "These windows are shared between Glucose Alarms and Device Alarms."
+                                            )
+                                        }
+                                    )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: { Image(systemName: "questionmark.circle") }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
+            }
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Day & Night Windows")
+            .navigationBarTitleDisplayMode(.inline)
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                    sheetTitle: String(localized: "Help")
+                )
+            }
+            .onAppear(perform: configureView)
+        }
+
+        private static func dateFromTimeOfDay(_ time: TimeOfDay) -> Date {
+            Calendar.current.date(bySettingHour: time.hour, minute: time.minute, second: 0, of: Date()) ?? Date()
+        }
+
+        private static func timeOfDay(from date: Date) -> TimeOfDay {
+            let comps = Calendar.current.dateComponents([.hour, .minute], from: date)
+            return TimeOfDay(hour: comps.hour ?? 0, minute: comps.minute ?? 0)
+        }
+
+        /// Locale-aware HH:mm / h:mm a. Used so 12-hour locales see "10:00 PM"
+        /// instead of "22:00" in hint copy.
+        private static func formattedTimeOfDay(hour: Int, minute: Int) -> String {
+            let date = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) ?? Date()
+            return date.formatted(.dateTime.hour().minute())
+        }
+    }
+}

+ 31 - 10
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -1,3 +1,4 @@
+import LoopKit
 import LoopKitUI
 import SwiftUI
 import Swinject
@@ -26,6 +27,26 @@ extension CGMSettings {
         @State private var simulatedScenarioRaw: String = UserDefaults.standard
             .string(forKey: "GlucoseSimulator.simulatedScenario") ?? SimulatedSensorScenario.runningNormally.rawValue
 
+        /// Routes "open URL failed" warnings through `TrioAlertManager` so
+        /// they share the same in-app banner UI as the rest of the alert
+        /// pipeline (no more SwiftMessages roundtrip).
+        private func warnOpenFailed(identifier: String, title: String, body: String) {
+            let content = Alert.Content(
+                title: title,
+                body: body,
+                acknowledgeActionButtonLabel: String(localized: "OK")
+            )
+            let alert = Alert(
+                identifier: Alert.Identifier(managerIdentifier: "trio.cgmSettings", alertIdentifier: identifier),
+                foregroundContent: content,
+                backgroundContent: content,
+                trigger: .immediate,
+                interruptionLevel: .active,
+                sound: nil
+            )
+            resolver.resolve(TrioAlertManager.self)?.issueAlert(alert)
+        }
+
         // Initialize state variables with defaults if needed
         private func initializeSimulatorSettings() {
             if centerValue == 0 {
@@ -69,11 +90,11 @@ extension CGMSettings {
                                 Button {
                                     UIApplication.shared.open(appURL, options: [:]) { success in
                                         if !success {
-                                            self.router.alertMessage
-                                                .send(MessageContent(
-                                                    content: "Unable to open the app",
-                                                    type: .warning
-                                                ))
+                                            warnOpenFailed(
+                                                identifier: "cgm.app.open.failed",
+                                                title: String(localized: "Open failed"),
+                                                body: String(localized: "Unable to open the app")
+                                            )
                                         }
                                     }
                                 }
@@ -156,11 +177,11 @@ extension CGMSettings {
                         Button {
                             UIApplication.shared.open(url, options: [:]) { success in
                                 if !success {
-                                    self.router.alertMessage
-                                        .send(MessageContent(
-                                            content: "No URL available",
-                                            type: .warning
-                                        ))
+                                    warnOpenFailed(
+                                        identifier: "nightscout.open.failed",
+                                        title: String(localized: "Open failed"),
+                                        body: String(localized: "No URL available")
+                                    )
                                 }
                             }
                         }

+ 27 - 1
Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -1,4 +1,5 @@
 import CoreData
+import LoopKit
 import Observation
 import SwiftDate
 import SwiftUI
@@ -7,6 +8,7 @@ extension Calibrations {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
         @ObservationIgnored @Injected() var calibrationService: CalibrationService!
+        @ObservationIgnored @Injected() var trioAlertManager: TrioAlertManager!
 
         var slope: Double = 1
         var intercept: Double = 1
@@ -77,7 +79,8 @@ extension Calibrations {
 
                     calibrationService.addCalibration(calibration)
                 } else {
-                    info(.service, "Glucose is stale for calibration")
+                    debug(.service, "Glucose is stale for calibration")
+                    issueStaleGlucoseAlert()
                     return
                 }
             } catch {
@@ -85,6 +88,29 @@ extension Calibrations {
             }
         }
 
+        /// Surfaces the "glucose too stale to calibrate against" condition as
+        /// a one-shot info alert through `TrioAlertManager`. Mirrors the old
+        /// `info(.service, …)` banner path that ran via `router.alertMessage`.
+        private func issueStaleGlucoseAlert() {
+            let content = Alert.Content(
+                title: String(localized: "Calibration unavailable"),
+                body: String(localized: "Glucose is stale for calibration"),
+                acknowledgeActionButtonLabel: String(localized: "OK")
+            )
+            let alert = Alert(
+                identifier: Alert.Identifier(
+                    managerIdentifier: "trio.calibration",
+                    alertIdentifier: "glucose.stale"
+                ),
+                foregroundContent: content,
+                backgroundContent: content,
+                trigger: .immediate,
+                interruptionLevel: .active,
+                sound: nil
+            )
+            trioAlertManager?.issueAlert(alert)
+        }
+
         func removeLast() {
             calibrationService.removeLast()
             setupCalibrations()

+ 7 - 0
Trio/Sources/Modules/DeviceAlarms/DeviceAlarmsDataFlow.swift

@@ -0,0 +1,7 @@
+import Combine
+
+enum DeviceAlarms {
+    enum Config {}
+}
+
+protocol DeviceAlarmsProvider: Provider {}

+ 5 - 0
Trio/Sources/Modules/DeviceAlarms/DeviceAlarmsProvider.swift

@@ -0,0 +1,5 @@
+import Foundation
+
+extension DeviceAlarms {
+    final class Provider: BaseProvider, DeviceAlarmsProvider {}
+}

+ 8 - 0
Trio/Sources/Modules/DeviceAlarms/DeviceAlarmsStateModel.swift

@@ -0,0 +1,8 @@
+import Combine
+import SwiftUI
+
+extension DeviceAlarms {
+    final class StateModel: BaseStateModel<Provider> {
+        override func subscribe() {}
+    }
+}

+ 65 - 0
Trio/Sources/Modules/DeviceAlarms/View/AddDeviceAlarmSheet.swift

@@ -0,0 +1,65 @@
+import SwiftUI
+
+struct AddDeviceAlarmSheet: View {
+    let onPick: (DeviceAlertSeverity) -> Void
+
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) private var colorScheme
+    @Environment(AppState.self) private var appState
+
+    var body: some View {
+        NavigationStack {
+            List {
+                Section {
+                    ForEach(DeviceAlertSeverity.allCases) { severity in
+                        Button {
+                            onPick(severity)
+                            dismiss()
+                        } label: {
+                            HStack(spacing: 12) {
+                                Image(systemName: icon(for: severity))
+                                    .foregroundStyle(tint(for: severity))
+                                    .frame(width: 28)
+                                VStack(alignment: .leading, spacing: 2) {
+                                    Text(severity.displayName)
+                                        .foregroundColor(.primary)
+                                    Text(severity.blurb)
+                                        .font(.footnote)
+                                        .foregroundColor(.secondary)
+                                }
+                                Spacer()
+                                Image(systemName: "chevron.right")
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                            }
+                        }
+                    }
+                }.listRowBackground(Color.chart)
+            }
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Add Variant")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button(String(localized: "Cancel")) { dismiss() }
+                }
+            }
+        }
+    }
+
+    private func icon(for severity: DeviceAlertSeverity) -> String {
+        switch severity {
+        case .critical: return "exclamationmark.triangle.fill"
+        case .timeSensitive: return "bell.badge.fill"
+        case .normal: return "bell.fill"
+        }
+    }
+
+    private func tint(for severity: DeviceAlertSeverity) -> Color {
+        switch severity {
+        case .critical: return .red
+        case .timeSensitive: return .orange
+        case .normal: return .accentColor
+        }
+    }
+}

+ 129 - 0
Trio/Sources/Modules/DeviceAlarms/View/DeviceAlarmEditorView.swift

@@ -0,0 +1,129 @@
+import LoopKit
+import SwiftUI
+
+struct DeviceAlarmEditorView: View {
+    @ObservedObject var store: DeviceAlertsStore
+    let configID: UUID
+    let isNew: Bool
+    var onDone: () -> Void
+    var onCancel: () -> Void
+
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) private var colorScheme
+    @Environment(AppState.self) private var appState
+    @State private var working: DeviceAlertSeverityConfig
+
+    init(
+        store: DeviceAlertsStore,
+        initial: DeviceAlertSeverityConfig,
+        isNew: Bool,
+        onDone: @escaping () -> Void,
+        onCancel: @escaping () -> Void = {}
+    ) {
+        self.store = store
+        configID = initial.id
+        self.isNew = isNew
+        self.onDone = onDone
+        self.onCancel = onCancel
+        _working = State(initialValue: initial)
+    }
+
+    var body: some View {
+        NavigationStack {
+            Form {
+                Section(
+                    header: Text("Behavior"),
+                    footer: Text(working.severity.blurb)
+                ) {
+                    HStack {
+                        Text(working.severity.displayName).font(.headline)
+                        Spacer()
+                        Text(activeLabel)
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                    }
+                    // Critical-tier configs are always armed — the user can
+                    // mute the sound via the Audio section but not turn the
+                    // alarm itself off. Other tiers expose the toggle.
+                    if working.severity != .critical {
+                        Toggle(String(localized: "Enabled"), isOn: $working.isEnabled)
+                    }
+                    Toggle(
+                        String(localized: "Override Silence & Focus Mode"),
+                        isOn: $working.overridesSilenceAndDND
+                    )
+                }.listRowBackground(Color.chart)
+
+                AlarmActiveSection(activeOption: $working.activeOption)
+                AlarmAudioSection(
+                    playsSound: $working.playsSound,
+                    soundFilename: $working.soundFilename
+                )
+
+                Section(header: Text("Applies To (Cannot be changed)")) {
+                    ForEach(conceptsForTier(working.severity), id: \.self) { concept in
+                        Text(concept.displayTitle)
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                    }
+                }.listRowBackground(Color.chart)
+
+                if !isNew, store.canDelete(working) {
+                    Section {
+                        Button(role: .destructive) {
+                            store.remove(working)
+                            dismiss()
+                        } label: {
+                            Text("Delete Variant")
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+            }
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle(working.severity.displayName)
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .confirmationAction) {
+                    Button(isNew ? String(localized: "Add") : String(localized: "Done")) {
+                        if isNew {
+                            store.add(working)
+                        } else {
+                            store.update(working)
+                        }
+                        onDone()
+                        dismiss()
+                    }
+                }
+                ToolbarItem(placement: .cancellationAction) {
+                    Button(String(localized: "Cancel")) {
+                        onCancel()
+                        dismiss()
+                    }
+                }
+            }
+        }
+    }
+
+    private var activeLabel: String {
+        switch working.activeOption {
+        case .always: return String(localized: "Day & Night")
+        case .day: return String(localized: "Day only")
+        case .night: return String(localized: "Night only")
+        }
+    }
+
+    /// Distinct alarm concepts whose catalog entries fall into this tier.
+    /// Sorted by display title so the list is stable across plugin changes.
+    private func conceptsForTier(_ tier: DeviceAlertSeverity) -> [LoopKit.Alert.CatalogConcept] {
+        var seen: Set<LoopKit.Alert.CatalogConcept> = []
+        var ordered: [LoopKit.Alert.CatalogConcept] = []
+        for entry in AlertCatalogRegistry.entries
+            where DeviceAlertSeverity(level: entry.interruptionLevel) == tier
+        {
+            if seen.insert(entry.concept).inserted {
+                ordered.append(entry.concept)
+            }
+        }
+        return ordered.sorted { $0.displayTitle < $1.displayTitle }
+    }
+}

+ 226 - 0
Trio/Sources/Modules/DeviceAlarms/View/DeviceAlarmsRootView.swift

@@ -0,0 +1,226 @@
+import SwiftUI
+import Swinject
+
+private enum DeviceAlarmSheet: Identifiable {
+    case picker
+    case editor(DeviceAlertSeverityConfig, isNew: Bool)
+    case help(DeviceAlertSeverity)
+
+    var id: String {
+        switch self {
+        case .picker: return "picker"
+        case let .editor(config, _): return config.id.uuidString
+        case let .help(severity): return "helpSheet_" + severity.id
+        }
+    }
+}
+
+extension DeviceAlarms {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @StateObject private var store = DeviceAlertsStore.shared
+
+        @State private var sheet: DeviceAlarmSheet?
+        @State private var pendingNewSeverity: DeviceAlertSeverity?
+
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: AnyView?
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            List {
+                ForEach(DeviceAlertSeverity.allCases) { severity in
+                    Section {
+                        VStack(alignment: .leading, spacing: 5) {
+                            HStack {
+                                Image(systemName: severityIcon(for: severity))
+                                    .foregroundStyle(severityTint(for: severity))
+                                Text(severity.displayName)
+                                Spacer()
+                            }.font(.headline)
+
+                            HStack(alignment: .center) {
+                                Text(severity.blurb)
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                                    .lineLimit(nil)
+                                    .fixedSize(horizontal: false, vertical: true)
+                                Spacer()
+                                Button(action: {
+                                    sheet = .help(severity)
+                                }) {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                                .buttonStyle(BorderlessButtonStyle())
+                            }.padding(.vertical, 5)
+                        }
+
+                        ForEach(store.configs(in: severity)) { config in
+                            row(for: config)
+                                .opacity(config.isEnabled ? 1 : 0.5)
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+
+                Section {
+                    Text("Day & Night Windows")
+                        .foregroundStyle(Color.accentColor)
+                        .navigationLink(to: .alarmWindows, from: self)
+                }.listRowBackground(Color.chart)
+            }
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Device Alarms")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .primaryAction) {
+                    Button { sheet = .picker } label: { Image(systemName: "plus") }
+                }
+            }
+            .sheet(item: $sheet, onDismiss: handleSheetDismiss) { which in
+                switch which {
+                case .picker:
+                    AddDeviceAlarmSheet { severity in
+                        pendingNewSeverity = severity
+                        sheet = nil
+                    }
+                case let .editor(config, isNew):
+                    DeviceAlarmEditorView(
+                        store: store,
+                        initial: config,
+                        isNew: isNew,
+                        onDone: { sheet = nil },
+                        onCancel: { sheet = nil }
+                    )
+                case let .help(severity):
+                    SettingInputHintView(
+                        hintDetent: $hintDetent,
+                        shouldDisplayHint: Binding(
+                            get: { sheet != nil },
+                            set: { if !$0 { sheet = nil } }
+                        ),
+                        hintLabel: String(
+                            localized: "\(severity.displayName) Device Alerts",
+                            comment: "Device Alerts help sheet label; text reads: '<severity level> Device Alerts'."
+                        ),
+                        hintText: selectedVerboseHint ?? AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(severity.hintText)
+                            }
+                        ),
+                        sheetTitle: String(localized: "Help", comment: "Help sheet title")
+                    )
+                }
+            }
+            .onAppear(perform: configureView)
+        }
+
+        private func handleSheetDismiss() {
+            guard let severity = pendingNewSeverity else { return }
+            pendingNewSeverity = nil
+            DispatchQueue.main.async {
+                let new = DeviceAlertSeverityConfig(
+                    severity: severity,
+                    activeOption: nextAvailableOption(for: severity)
+                )
+                sheet = .editor(new, isNew: true)
+            }
+        }
+
+        // MARK: - Row
+
+        @ViewBuilder private func row(for config: DeviceAlertSeverityConfig) -> some View {
+            Button {
+                sheet = .editor(config, isNew: false)
+            } label: {
+                HStack(spacing: 12) {
+                    AlarmWindowIcon(option: config.activeOption)
+                        .font(.title3)
+                    VStack(alignment: .leading, spacing: 2) {
+                        Text(windowLabel(for: config.activeOption))
+                            .foregroundColor(.primary)
+                        soundSummary(for: config)
+                    }
+                    Spacer()
+                    Image(systemName: "chevron.right")
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                }
+            }
+            .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                if store.canDelete(config) {
+                    Button(role: .destructive) {
+                        store.remove(config)
+                    } label: {
+                        Label("Delete", systemImage: "trash")
+                    }
+                }
+            }
+        }
+
+        private func soundSummary(for config: DeviceAlertSeverityConfig) -> some View {
+            // Show the sound and override facts independently. Previously
+            // the override badge was hidden when sound was off, but "sound
+            // off + override on" is a valid combo (silent + haptic that
+            // breaks through Focus / Sleep) and the user needs to see it.
+            HStack(spacing: 6) {
+                HStack(spacing: 4) {
+                    Image(systemName: config.playsSound ? "speaker.wave.2.fill" : "speaker.slash.fill")
+                    Text(config.playsSound ? "Sound on" : "Sound off")
+                }
+                if config.overridesSilenceAndDND {
+                    Text("·")
+                    HStack(spacing: 4) {
+                        Image(systemName: "bell.badge.fill")
+                        Text("Overrides Focus")
+                    }
+                }
+            }
+            .font(.footnote)
+            .foregroundColor(.secondary)
+        }
+
+        // MARK: - Helpers
+
+        private func windowLabel(for option: ActiveOption) -> String {
+            switch option {
+            case .always: return String(localized: "Day & Night")
+            case .day: return String(localized: "Day only")
+            case .night: return String(localized: "Night only")
+            }
+        }
+
+        private func severityIcon(for severity: DeviceAlertSeverity) -> String {
+            switch severity {
+            case .critical: return "exclamationmark.triangle.fill"
+            case .timeSensitive: return "bell.badge.fill"
+            case .normal: return "bell.fill"
+            }
+        }
+
+        private func severityTint(for severity: DeviceAlertSeverity) -> Color {
+            switch severity {
+            case .critical: return .red
+            case .timeSensitive: return .orange
+            case .normal: return .accentColor
+            }
+        }
+
+        /// Suggest an `ActiveOption` not yet used in this severity tier so the
+        /// editor opens on a meaningful new variant instead of duplicating
+        /// the existing `.always` row.
+        private func nextAvailableOption(for severity: DeviceAlertSeverity) -> ActiveOption {
+            let used = Set(store.configs(in: severity).map(\.activeOption))
+            for candidate in [ActiveOption.day, .night, .always] where !used.contains(candidate) {
+                return candidate
+            }
+            return .day
+        }
+    }
+}

+ 7 - 0
Trio/Sources/Modules/GlucoseAlerts/GlucoseAlertsDataFlow.swift

@@ -0,0 +1,7 @@
+import Combine
+
+enum GlucoseAlerts {
+    enum Config {}
+}
+
+protocol GlucoseAlertsProvider: Provider {}

+ 5 - 0
Trio/Sources/Modules/GlucoseAlerts/GlucoseAlertsProvider.swift

@@ -0,0 +1,5 @@
+import Foundation
+
+extension GlucoseAlerts {
+    final class Provider: BaseProvider, GlucoseAlertsProvider {}
+}

+ 36 - 0
Trio/Sources/Modules/GlucoseAlerts/GlucoseAlertsStateModel.swift

@@ -0,0 +1,36 @@
+import Combine
+import SwiftUI
+
+extension GlucoseAlerts {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var fetchGlucoseManager: FetchGlucoseManager!
+
+        @Published var units: GlucoseUnits = .mgdL
+        @Published var glucoseBadge = false
+        @Published var cgmAppInfo: CGMManagerAlertOwnership.OwningApp?
+
+        var cgmProvidesOwnAlerts: Bool { cgmAppInfo != nil }
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+            refreshCGMOwnership()
+            subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
+        }
+
+        /// Re-read whether the active CGM owns its own glucose alerts. The
+        /// active manager can change while the user is in CGM settings, so
+        /// the alarms view calls this on appear.
+        func refreshCGMOwnership() {
+            cgmAppInfo = CGMManagerAlertOwnership.owningApp(
+                manager: fetchGlucoseManager?.cgmManager,
+                sourceType: fetchGlucoseManager?.cgmGlucoseSourceType ?? .none
+            )
+        }
+    }
+}
+
+extension GlucoseAlerts.StateModel: SettingsObserver {
+    func settingsDidChange(_: TrioSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 87 - 0
Trio/Sources/Modules/GlucoseAlerts/View/AddGlucoseAlertSheet.swift

@@ -0,0 +1,87 @@
+import SwiftUI
+
+struct AddGlucoseAlertSheet: View {
+    @ObservedObject var store: GlucoseAlertsStore
+    let onPick: (GlucoseAlertType) -> Void
+
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) private var colorScheme
+    @Environment(AppState.self) private var appState
+
+    var body: some View {
+        NavigationStack {
+            List {
+                if !availableTypes.isEmpty {
+                    Section(header: Text("Available")) {
+                        ForEach(availableTypes) { type in
+                            availableRow(for: type)
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+                if !unavailableTypes.isEmpty {
+                    Section(
+                        header: Text("Unavailable"),
+                        footer: Text(
+                            "Already set for Day & Night. Change the alarm time window to add a second alarm for the same type."
+                        )
+                    ) {
+                        ForEach(unavailableTypes) { type in
+                            unavailableRow(for: type)
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+            }
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Add Alarm")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button(String(localized: "Cancel")) { dismiss() }
+                }
+            }
+        }
+    }
+
+    private var availableTypes: [GlucoseAlertType] {
+        GlucoseAlertType.allCases.filter { !store.availableActiveOptions(forNewAlarmOfType: $0).isEmpty }
+    }
+
+    private var unavailableTypes: [GlucoseAlertType] {
+        GlucoseAlertType.allCases.filter { store.availableActiveOptions(forNewAlarmOfType: $0).isEmpty }
+    }
+
+    private func availableRow(for type: GlucoseAlertType) -> some View {
+        Button {
+            onPick(type)
+            dismiss()
+        } label: {
+            HStack(spacing: 12) {
+                VStack(alignment: .leading, spacing: 2) {
+                    Text(type.displayName)
+                        .foregroundColor(.primary)
+                    Text(type.blurb)
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                }
+                Spacer()
+                Image(systemName: "chevron.right")
+                    .font(.footnote)
+                    .foregroundColor(.secondary)
+            }
+        }
+    }
+
+    private func unavailableRow(for type: GlucoseAlertType) -> some View {
+        HStack(spacing: 12) {
+            VStack(alignment: .leading, spacing: 2) {
+                Text(type.displayName)
+                    .foregroundColor(.primary)
+                Text(type.blurb)
+                    .font(.footnote)
+                    .foregroundColor(.secondary)
+            }
+            Spacer()
+        }
+        .opacity(0.5)
+    }
+}

+ 23 - 0
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmActiveSection.swift

@@ -0,0 +1,23 @@
+import SwiftUI
+
+struct AlarmActiveSection: View {
+    @Binding var activeOption: ActiveOption
+    /// Subset of `ActiveOption` cases the picker is allowed to offer.
+    /// Defaults to all cases when omitted.
+    var allowed: [ActiveOption] = ActiveOption.allCases
+
+    var body: some View {
+        Section(
+            header: Text("Active During"),
+            footer: Text(
+                "Day and Night windows are configured globally on the Alarms screen."
+            )
+        ) {
+            AlarmEnumMenuPicker(
+                title: String(localized: "Active"),
+                selection: $activeOption,
+                allowed: allowed
+            )
+        }.listRowBackground(Color.chart)
+    }
+}

+ 168 - 0
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmAudioSection.swift

@@ -0,0 +1,168 @@
+import AVFoundation
+import SwiftUI
+
+struct AlarmAudioSection: View {
+    @Binding var playsSound: Bool
+    @Binding var soundFilename: String
+
+    @State private var showTonePicker = false
+
+    var body: some View {
+        Section(header: Text("Alert Sound")) {
+            Toggle("Play Sound", isOn: $playsSound)
+
+            if playsSound {
+                Button {
+                    showTonePicker = true
+                } label: {
+                    HStack {
+                        Text("Tone")
+                        Spacer()
+                        Text(AlarmSoundCatalog.displayName(for: soundFilename))
+                            .foregroundColor(.secondary)
+                        Image(systemName: "chevron.right")
+                            .foregroundColor(.secondary)
+                            .font(.footnote)
+                    }
+                }
+                .buttonStyle(.plain)
+                .sheet(isPresented: $showTonePicker) {
+                    TonePickerSheet(selected: $soundFilename)
+                }
+            }
+        }
+        .listRowBackground(Color.chart)
+    }
+}
+
+private struct TonePickerSheet: View {
+    @Binding var selected: String
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) private var colorScheme
+    @Environment(AppState.self) private var appState
+    @StateObject private var previewer = SoundPreviewPlayer()
+
+    var body: some View {
+        NavigationStack {
+            List {
+                Section {
+                    ForEach(AlarmSoundCatalog.allFilenames, id: \.self) { filename in
+                        TonePickerRow(
+                            filename: filename,
+                            selected: $selected,
+                            previewer: previewer
+                        )
+                    }
+                }.listRowBackground(Color.chart)
+            }
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Choose Tone")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .confirmationAction) {
+                    Button("Done") {
+                        previewer.stop()
+                        dismiss()
+                    }
+                }
+            }
+            .onDisappear { previewer.stop() }
+        }
+    }
+}
+
+private struct TonePickerRow: View {
+    let filename: String
+    @Binding var selected: String
+    @ObservedObject var previewer: SoundPreviewPlayer
+
+    var body: some View {
+        HStack(spacing: 12) {
+            Button {
+                selected = filename
+            } label: {
+                Text(AlarmSoundCatalog.displayName(for: filename))
+                    .foregroundColor(.primary)
+                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .contentShape(Rectangle())
+            }
+            .buttonStyle(.plain)
+
+            Button {
+                previewer.toggle(filename: filename)
+            } label: {
+                Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
+                    .font(.title3)
+                    .foregroundColor(.accentColor)
+            }
+            .buttonStyle(.plain)
+
+            Spacer()
+
+            Image(systemName: "checkmark")
+                .foregroundColor(.accentColor)
+                .opacity(filename == selected ? 1 : 0)
+                .frame(width: 20)
+        }
+    }
+
+    private var isPlaying: Bool { previewer.playing == filename }
+}
+
+/// Plays bundled alarm `.caf` files for in-picker auditioning. Mixable
+/// `.playback` session so it ducks other audio without taking it over.
+/// Not the critical-alert player — this one obeys the silent switch.
+@MainActor private final class SoundPreviewPlayer: ObservableObject {
+    @Published private(set) var playing: String?
+    private var player: AVAudioPlayer?
+
+    func toggle(filename: String) {
+        if playing == filename {
+            stop()
+        } else {
+            play(filename: filename)
+        }
+    }
+
+    func play(filename: String) {
+        stop()
+        let resource = (filename as NSString).deletingPathExtension
+        let ext = (filename as NSString).pathExtension.isEmpty ? "caf" : (filename as NSString).pathExtension
+        guard let url = Bundle.main.url(forResource: resource, withExtension: ext, subdirectory: "Sounds") else {
+            return
+        }
+        do {
+            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
+            try AVAudioSession.sharedInstance().setActive(true, options: [])
+            let p = try AVAudioPlayer(contentsOf: url)
+            p.delegate = previewDelegate
+            p.prepareToPlay()
+            guard p.play() else { return }
+            player = p
+            playing = filename
+        } catch {
+            debug(.service, "Audio preview failed for \(filename): \(error)")
+        }
+    }
+
+    func stop() {
+        player?.stop()
+        player = nil
+        playing = nil
+        try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
+    }
+
+    private lazy var previewDelegate: PreviewDelegate = {
+        let delegate = PreviewDelegate()
+        delegate.onFinish = { [weak self] in self?.stop() }
+        return delegate
+    }()
+}
+
+private final class PreviewDelegate: NSObject, AVAudioPlayerDelegate {
+    var onFinish: (() -> Void)?
+
+    func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) {
+        Task { @MainActor in self.onFinish?() }
+    }
+}

+ 54 - 0
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmBGSection.swift

@@ -0,0 +1,54 @@
+import SwiftUI
+
+struct AlarmBGSection: View {
+    let header: String
+    let footer: String?
+    let title: String
+    let range: ClosedRange<Decimal>
+    let step: Decimal
+    let units: GlucoseUnits
+    @Binding var valueMgDL: Decimal
+
+    @State private var showPicker = false
+
+    var body: some View {
+        Section(
+            header: Text(header),
+            footer: footer.map { Text($0) }
+        ) {
+            VStack(spacing: 0) {
+                HStack {
+                    Text(title)
+                    Spacer()
+                    Text(valueMgDL.formatted(for: units))
+                        .foregroundColor(showPicker ? .accentColor : .primary)
+                    Text(units.rawValue)
+                        .foregroundColor(.secondary)
+                }
+                .contentShape(Rectangle())
+                .onTapGesture { showPicker.toggle() }
+
+                if showPicker {
+                    Picker(title, selection: $valueMgDL) {
+                        ForEach(pickerValues, id: \.self) { value in
+                            Text(value.formatted(for: units)).tag(value)
+                        }
+                    }
+                    .pickerStyle(.wheel)
+                    .frame(maxWidth: .infinity)
+                }
+            }
+        }.listRowBackground(Color.chart)
+    }
+
+    private var pickerValues: [Decimal] {
+        let setting = PickerSetting(
+            value: valueMgDL,
+            step: step,
+            min: range.lowerBound,
+            max: range.upperBound,
+            type: .glucose
+        )
+        return PickerSettingsProvider.shared.generatePickerValues(from: setting, units: units)
+    }
+}

+ 39 - 0
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmEnumMenuPicker.swift

@@ -0,0 +1,39 @@
+import SwiftUI
+
+struct AlarmEnumMenuPicker<E: CaseIterable & Hashable & DayNightDisplayable>: View {
+    let title: String
+    @Binding var selection: E
+    let allowed: [E]
+
+    init(title: String, selection: Binding<E>, allowed: [E]) {
+        self.title = title
+        _selection = selection
+        self.allowed = allowed
+    }
+
+    init(title: String, selection: Binding<E>) where E.AllCases: RandomAccessCollection {
+        self.title = title
+        _selection = selection
+        allowed = Array(E.allCases)
+    }
+
+    var body: some View {
+        HStack {
+            Text(title)
+            Spacer()
+            Picker("", selection: $selection) {
+                ForEach(allowed, id: \.self) { option in
+                    Text(option.displayName).tag(option)
+                }
+            }
+            .pickerStyle(.menu)
+            .onAppear(perform: validate)
+            .onChange(of: allowed) { _, _ in validate() }
+        }
+    }
+
+    private func validate() {
+        guard !allowed.contains(selection), let first = allowed.first else { return }
+        selection = first
+    }
+}

+ 47 - 0
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmGramsSection.swift

@@ -0,0 +1,47 @@
+import SwiftUI
+
+/// Mirror of `AlarmBGSection` but for gram-valued thresholds (carbsRequired).
+/// Single fixed unit, no mg/dL ↔︎ mmol/L conversion.
+struct AlarmGramsSection: View {
+    let header: String
+    let footer: String?
+    let title: String
+    let range: ClosedRange<Int>
+    let step: Int
+    @Binding var valueGrams: Decimal
+
+    @State private var showPicker = false
+
+    var body: some View {
+        Section(
+            header: Text(header),
+            footer: footer.map { Text($0) }
+        ) {
+            VStack(spacing: 0) {
+                HStack {
+                    Text(title)
+                    Spacer()
+                    Text("\(Int(NSDecimalNumber(decimal: valueGrams).intValue))")
+                        .foregroundColor(showPicker ? .accentColor : .primary)
+                    Text(String(localized: "g", comment: "Abbreviation for grams"))
+                        .foregroundColor(.secondary)
+                }
+                .contentShape(Rectangle())
+                .onTapGesture { showPicker.toggle() }
+
+                if showPicker {
+                    Picker(title, selection: Binding(
+                        get: { Int(NSDecimalNumber(decimal: valueGrams).intValue) },
+                        set: { valueGrams = Decimal($0) }
+                    )) {
+                        ForEach(Array(stride(from: range.lowerBound, through: range.upperBound, by: step)), id: \.self) { v in
+                            Text("\(v)").tag(v)
+                        }
+                    }
+                    .pickerStyle(.wheel)
+                    .frame(maxWidth: .infinity)
+                }
+            }
+        }.listRowBackground(Color.chart)
+    }
+}

+ 44 - 0
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmWindowIcon.swift

@@ -0,0 +1,44 @@
+import SwiftUI
+
+/// Small SF Symbol that visually conveys an alarm's `ActiveOption` window:
+/// sun for day-only, moon-and-stars for night-only, overlapped pair for both.
+/// The frame is locked to the widest variant (the overlapped pair) so alarm
+/// titles don't shift horizontally when alarms with different windows are
+/// listed together.
+struct AlarmWindowIcon: View {
+    let option: ActiveOption
+
+    var body: some View {
+        ZStack {
+            // Hidden width anchor — always the widest variant. `.hidden()`
+            // keeps it in the layout but invisible, so the frame size scales
+            // with the caller's font (Dynamic Type-friendly) without
+            // hardcoding a width.
+            HStack(spacing: -10) {
+                Image(systemName: "sun.max.fill")
+                Image(systemName: "moon.stars.fill")
+            }
+            .hidden()
+
+            content
+        }
+    }
+
+    @ViewBuilder private var content: some View {
+        switch option {
+        case .day:
+            Image(systemName: "sun.max.fill")
+                .foregroundStyle(.orange)
+        case .night:
+            Image(systemName: "moon.stars.fill")
+                .foregroundStyle(.indigo)
+        case .always:
+            HStack(spacing: -10) {
+                Image(systemName: "sun.max.fill")
+                    .foregroundStyle(.orange)
+                Image(systemName: "moon.stars.fill")
+                    .foregroundStyle(.indigo)
+            }
+        }
+    }
+}

+ 191 - 0
Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertEditorView.swift

@@ -0,0 +1,191 @@
+import SwiftUI
+
+struct GlucoseAlertEditorView: View {
+    @ObservedObject var store: GlucoseAlertsStore
+    let alertID: UUID
+    let isNew: Bool
+
+    /// Windows the user can still pick without overlapping another alarm of
+    /// the same type. The being-edited alarm is excluded from "taken" so its
+    /// current option stays valid.
+    private var allowedActiveOptions: [ActiveOption] {
+        let available = store.availableActiveOptions(
+            forType: working.type,
+            excludingAlertID: isNew ? nil : alertID
+        )
+        return ActiveOption.allCases.filter { available.contains($0) }
+    }
+
+    let units: GlucoseUnits
+    var onDone: () -> Void
+    var onCancel: () -> Void
+
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) private var colorScheme
+    @Environment(AppState.self) private var appState
+    @State private var working: GlucoseAlert
+
+    init(
+        store: GlucoseAlertsStore,
+        initial: GlucoseAlert,
+        isNew: Bool,
+        units: GlucoseUnits,
+        onDone: @escaping () -> Void,
+        onCancel: @escaping () -> Void = {}
+    ) {
+        self.store = store
+        alertID = initial.id
+        self.isNew = isNew
+        self.units = units
+        self.onDone = onDone
+        self.onCancel = onCancel
+        _working = State(initialValue: initial)
+    }
+
+    var body: some View {
+        NavigationStack {
+            Form {
+                generalSection
+
+                switch working.type {
+                case .urgentLow: urgentLowBody
+                case .low: lowBody
+                case .forecastedLow: forecastedLowBody
+                case .high: highBody
+                case .carbsRequired: carbsRequiredBody
+                }
+
+                AlarmActiveSection(
+                    activeOption: $working.activeOption,
+                    allowed: allowedActiveOptions
+                )
+                AlarmAudioSection(
+                    playsSound: $working.playsSound,
+                    soundFilename: $working.soundFilename
+                )
+
+                if !isNew, store.canDelete(working) {
+                    Section {
+                        Button(role: .destructive) {
+                            store.remove(working)
+                            dismiss()
+                        } label: {
+                            Text("Delete Alarm")
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+            }
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle(working.type.displayName)
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .confirmationAction) {
+                    Button(isNew ? String(localized: "Add") : String(localized: "Done")) {
+                        if isNew {
+                            store.add(working)
+                        } else {
+                            store.update(working)
+                        }
+                        onDone()
+                        dismiss()
+                    }
+                }
+                ToolbarItem(placement: .cancellationAction) {
+                    Button(String(localized: "Cancel")) {
+                        onCancel()
+                        dismiss()
+                    }
+                }
+            }
+        }
+    }
+
+    // MARK: - Sections
+
+    private var generalSection: some View {
+        Section(
+            header: Text("General"),
+            footer: Text(working.type.blurb)
+        ) {
+            TextField(String(localized: "Name"), text: $working.name)
+            // Urgent-low is the safety floor — the user can mute the sound
+            // from the Audio section but the alarm itself is always armed.
+            if working.type != .urgentLow {
+                Toggle(String(localized: "Enabled"), isOn: $working.isEnabled)
+            }
+            Toggle(
+                String(localized: "Override Silence & Focus Mode"),
+                isOn: $working.overridesSilenceAndDND
+            )
+        }.listRowBackground(Color.chart)
+    }
+
+    private var urgentLowBody: some View {
+        AlarmBGSection(
+            header: String(localized: "Urgent Low Threshold"),
+            footer: String(
+                localized: "Recommended to always override silence and Focus mode."
+            ),
+            title: String(localized: "Glucose"),
+            range: 54 ... 80,
+            step: 1,
+            units: units,
+            valueMgDL: $working.thresholdMgDL
+        )
+    }
+
+    private var lowBody: some View {
+        AlarmBGSection(
+            header: String(localized: "Low Threshold"),
+            footer: String(
+                localized: "Fires when glucose is at or below this value."
+            ),
+            title: String(localized: "Glucose"),
+            range: 54 ... 100,
+            step: 1,
+            units: units,
+            valueMgDL: $working.thresholdMgDL
+        )
+    }
+
+    private var forecastedLowBody: some View {
+        AlarmBGSection(
+            header: String(localized: "Low Threshold"),
+            footer: String(
+                localized: "Fires when the forecast at +20 minutes (blended across all available prediction curves) is at or below this value."
+            ),
+            title: String(localized: "Glucose"),
+            range: 54 ... 100,
+            step: 1,
+            units: units,
+            valueMgDL: $working.thresholdMgDL
+        )
+    }
+
+    private var highBody: some View {
+        AlarmBGSection(
+            header: String(localized: "High Threshold"),
+            footer: String(
+                localized: "Fires when glucose is at or above this value."
+            ),
+            title: String(localized: "Glucose"),
+            range: 100 ... 400,
+            step: 1,
+            units: units,
+            valueMgDL: $working.thresholdMgDL
+        )
+    }
+
+    private var carbsRequiredBody: some View {
+        AlarmGramsSection(
+            header: String(localized: "Carbs Required Threshold"),
+            footer: String(
+                localized: "Fires when the algorithm suggests to eat at least this many grams of carbs to avoid a low."
+            ),
+            title: String(localized: "Carbs"),
+            range: 5 ... 50,
+            step: 1,
+            valueGrams: $working.thresholdMgDL
+        )
+    }
+}

+ 345 - 0
Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertsRootView.swift

@@ -0,0 +1,345 @@
+import SwiftUI
+import Swinject
+
+private enum AlertSheet: Identifiable {
+    case picker
+    case editor(GlucoseAlert, isNew: Bool)
+
+    var id: String {
+        switch self {
+        case .picker: return "picker"
+        case let .editor(alert, _): return alert.id.uuidString
+        }
+    }
+}
+
+extension GlucoseAlerts {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @StateObject private var store = GlucoseAlertsStore.shared
+
+        @State private var sheet: AlertSheet?
+        @State private var pendingNewType: GlucoseAlertType?
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: AnyView?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+        @State private var displayPickerLowGlucose: Bool = false
+        @State private var displayPickerHighGlucose: Bool = false
+
+        var body: some View {
+            List {
+                if !enabledAlerts.isEmpty {
+                    Section(header: Text("Enabled")) {
+                        ForEach(enabledAlerts) { alarm in
+                            row(for: alarm)
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+                if !disabledAlerts.isEmpty {
+                    Section(header: Text("Disabled")) {
+                        ForEach(disabledAlerts) { alarm in
+                            row(for: alarm).opacity(0.6)
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+                if !cgmHandledAlerts.isEmpty {
+                    Section(
+                        header: Text("Handled by CGM App"),
+                        footer: cgmHandledFooter
+                    ) {
+                        ForEach(cgmHandledAlerts) { alarm in
+                            row(for: alarm).opacity(0.6)
+                        }
+                    }.listRowBackground(Color.chart)
+                }
+
+                // FIXME: make this into a nice setting with mini and verbose hint
+                Section {
+                    Text("Day & Night Windows")
+                        .foregroundStyle(Color.accentColor)
+                        .navigationLink(to: .alarmWindows, from: self)
+                }.listRowBackground(Color.chart)
+
+                Section(footer: Text(useCGMAlertsFooter)) {
+                    Toggle(isOn: Binding(
+                        // When the active CGM has no companion app to defer
+                        // to, force the visible state OFF regardless of the
+                        // stored preference — there's nothing for the toggle
+                        // to control, so showing it ON would mislead.
+                        get: {
+                            guard state.cgmProvidesOwnAlerts else { return false }
+                            return !store.configuration.forceTrioAlertsWhenCGMProvidesOwn
+                        },
+                        set: { store.configuration.forceTrioAlertsWhenCGMProvidesOwn = !$0 }
+                    )) {
+                        Text("Use CGM App Alerts")
+                    }
+                    .disabled(!state.cgmProvidesOwnAlerts)
+                }.listRowBackground(Color.chart)
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.glucoseBadge,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "Show Glucose App Badge")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: String(localized: "Show Glucose App Badge"),
+                    miniHint: String(localized: "Show your current glucose on Trio app icon."),
+                    verboseHint: VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: OFF").bold()
+                        Text(
+                            "This will add your current glucose on the top right of your Trio icon as a red notification badge. Changing setting takes effect on next Glucose reading."
+                        )
+                    },
+                    headerText: String(localized: "Glucose App Badge")
+                )
+            }
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Glucose Alarms")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .primaryAction) {
+                    Button { sheet = .picker } label: { Image(systemName: "plus") }
+                }
+            }
+            .sheet(item: $sheet, onDismiss: handleSheetDismiss) { which in
+                switch which {
+                case .picker:
+                    AddGlucoseAlertSheet(store: store) { type in
+                        pendingNewType = type
+                        sheet = nil
+                    }
+                case let .editor(alarm, isNew):
+                    GlucoseAlertEditorView(
+                        store: store,
+                        initial: alarm,
+                        isNew: isNew,
+                        units: state.units,
+                        onDone: { sheet = nil },
+                        onCancel: { sheet = nil }
+                    )
+                }
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                    sheetTitle: String(localized: "Help", comment: "Help sheet title")
+                )
+            }
+            .onAppear {
+                configureView()
+                state.refreshCGMOwnership()
+            }
+        }
+
+        private func handleSheetDismiss() {
+            guard let type = pendingNewType else { return }
+            pendingNewType = nil
+            DispatchQueue.main.async {
+                var seed = GlucoseAlert(type: type)
+                // Default new alarm to the first available window so it
+                // doesn't overlap with whatever is already configured.
+                let available = store.availableActiveOptions(forNewAlarmOfType: type)
+                if let first = ActiveOption.allCases.first(where: available.contains) {
+                    seed.activeOption = first
+                }
+                sheet = .editor(seed, isNew: true)
+            }
+        }
+
+        // MARK: - Sorted lists
+
+        /// Mirror of `GlucoseAlertCoordinator.shouldRespect(alarm:)`'s
+        /// CGM-ownership branch: when "Use CGM App Alerts" is ON and the
+        /// active CGM provides its own glucose alerts, the coordinator
+        /// silences reading-driven types. The view surfaces this by moving
+        /// those alarms into a dedicated section.
+        private var isCGMSuppressionActive: Bool {
+            !store.configuration.forceTrioAlertsWhenCGMProvidesOwn && state.cgmProvidesOwnAlerts
+        }
+
+        private var cgmHandledAlerts: [GlucoseAlert] {
+            guard isCGMSuppressionActive else { return [] }
+            return store.alerts
+                .filter { $0.isEnabled && $0.type.isReadingDriven }
+                .sorted { lhs, rhs in
+                    lhs.type.priority < rhs.type.priority
+                }
+        }
+
+        /// Footer for the "Use CGM App Alerts" toggle. Names the eligible
+        /// CGMs when one of them is active and the user can decide; when
+        /// the active CGM has no companion app the toggle is disabled and
+        /// the footer says Trio is handling alarms.
+        private var useCGMAlertsFooter: String {
+            if state.cgmProvidesOwnAlerts {
+                return String(
+                    localized:
+                    "Your CGM app handles alerts (Dexcom G6 / One, G7 / One+, or xDrip4iOS). Turn off to let Trio alert you."
+                )
+            }
+            return String(
+                localized:
+                "Your CGM has no companion app, so Trio handles alarms."
+            )
+        }
+
+        /// Footer for the "Handled by CGM App" section. Names the specific
+        /// companion app, and renders its name as a deep link when a URL
+        /// scheme is known for that app (see CGMManagerAlertOwnership).
+        @ViewBuilder private var cgmHandledFooter: some View {
+            if let info = state.cgmAppInfo {
+                Text(handledFooterMarkdown(for: info))
+            } else {
+                Text(
+                    "These alarms are silenced because the CGM app handles them. To have Trio notify you instead, turn off \"Use CGM App Alerts\" below."
+                )
+            }
+        }
+
+        private func handledFooterMarkdown(for info: CGMManagerAlertOwnership.OwningApp) -> AttributedString {
+            let body = String(
+                format: String(
+                    localized:
+                    "These alarms are silenced because the %@ app handles CGM alerts. To have Trio notify you instead, turn off \"Use CGM App Alerts\" below."
+                ),
+                "{{NAME}}"
+            )
+            var result = AttributedString(body)
+            if let range = result.range(of: "{{NAME}}") {
+                var name = AttributedString(info.name)
+                if let url = info.deepLink {
+                    name.link = url
+                }
+                result.replaceSubrange(range, with: name)
+            }
+            return result
+        }
+
+        private var enabledAlerts: [GlucoseAlert] {
+            let handled = Set(cgmHandledAlerts.map(\.id))
+            return store.alerts
+                .filter(\.isEnabled)
+                .filter { !handled.contains($0.id) }
+                .sorted { lhs, rhs in
+                    lhs.type.priority < rhs.type.priority
+                }
+        }
+
+        private var disabledAlerts: [GlucoseAlert] {
+            store.alerts
+                .filter { !$0.isEnabled }
+                .sorted { lhs, rhs in
+                    lhs.type.priority < rhs.type.priority
+                }
+        }
+
+        // MARK: - Row
+
+        @ViewBuilder private func row(for alarm: GlucoseAlert) -> some View {
+            Button {
+                sheet = .editor(alarm, isNew: false)
+            } label: {
+                HStack(spacing: 12) {
+                    AlarmWindowIcon(option: alarm.activeOption)
+                        .font(.title3)
+
+                    VStack(alignment: .leading, spacing: 2) {
+                        Text(alarm.name)
+                            .foregroundColor(.primary)
+                        Text(summary(for: alarm))
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                        soundSummary(for: alarm)
+                    }
+
+                    Spacer()
+
+                    Image(systemName: "chevron.right")
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                }
+            }
+            .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                if store.canDelete(alarm) {
+                    Button(role: .destructive) {
+                        store.remove(alarm)
+                    } label: {
+                        Label("Delete", systemImage: "trash")
+                    }
+                }
+            }
+        }
+
+        private func summary(for alarm: GlucoseAlert) -> String {
+            let comparator: String = {
+                switch alarm.type {
+                case .high: return String(localized: "above")
+                case .carbsRequired: return String(localized: "at least")
+                default: return String(localized: "below")
+                }
+            }()
+            let threshold: String = {
+                // `thresholdMgDL` stores grams for carbsRequired — no
+                // mg/dL ↔ mmol/L conversion and a fixed "g" unit label.
+                if alarm.type == .carbsRequired {
+                    return "\(alarm.thresholdMgDL) \(String(localized: "g", comment: "gram of carbs"))"
+                }
+                return "\(alarm.thresholdMgDL.formatted(for: state.units)) \(state.units.rawValue)"
+            }()
+            let window = AlarmEnumDescription.description(for: alarm.activeOption)
+            return "\(comparator.localizedCapitalized) \(threshold) • \(window)"
+        }
+
+        private func soundSummary(for alarm: GlucoseAlert) -> some View {
+            // Show the sound and override facts independently. Previously
+            // the override badge was hidden when sound was off, but "sound
+            // off + override on" is a valid combo (silent + haptic that
+            // breaks through Focus / Sleep) and the user needs to see it.
+            HStack(spacing: 6) {
+                HStack(spacing: 4) {
+                    Image(systemName: alarm.playsSound ? "speaker.wave.2.fill" : "speaker.slash.fill")
+                    Text(alarm.playsSound ? "Sound on" : "Sound off")
+                }
+                if alarm.overridesSilenceAndDND {
+                    Text("·")
+                    HStack(spacing: 4) {
+                        Image(systemName: "bell.badge.fill")
+                        Text("Overrides Focus")
+                    }
+                }
+            }
+            .font(.footnote)
+            .foregroundColor(.secondary)
+        }
+    }
+}
+
+private enum AlarmEnumDescription {
+    static func description(for option: ActiveOption) -> String {
+        switch option {
+        case .always: return String(localized: "Day & Night")
+        case .day: return String(localized: "Day only")
+        case .night: return String(localized: "Night only")
+        }
+    }
+}

+ 0 - 5
Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsDataFlow.swift

@@ -1,5 +0,0 @@
-enum GlucoseNotificationSettings {
-    enum Config {}
-}
-
-protocol GlucoseNotificationSettingsProvider {}

+ 0 - 3
Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsProvider.swift

@@ -1,3 +0,0 @@
-extension GlucoseNotificationSettings {
-    final class Provider: BaseProvider, GlucoseNotificationSettingsProvider {}
-}

+ 0 - 53
Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -1,53 +0,0 @@
-import SwiftUI
-
-extension GlucoseNotificationSettings {
-    final class StateModel: BaseStateModel<Provider> {
-        @Published var glucoseBadge = false
-        @Published var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
-        @Published var addSourceInfoToGlucoseNotifications = false
-        @Published var lowGlucose: Decimal = 0
-        @Published var highGlucose: Decimal = 0
-
-        @Published var notificationsPump = true
-        @Published var notificationsCgm = true
-        @Published var notificationsCarb = true
-        @Published var notificationsAlgorithm = true
-
-        var units: GlucoseUnits = .mgdL
-
-        override func subscribe() {
-            let units = settingsManager.settings.units
-            self.units = units
-
-            subscribeSetting(\.notificationsPump, on: $notificationsPump) { notificationsPump = $0 }
-            subscribeSetting(\.notificationsCgm, on: $notificationsCgm) { notificationsCgm = $0 }
-            subscribeSetting(\.notificationsCarb, on: $notificationsCarb) { notificationsCarb = $0 }
-            subscribeSetting(\.notificationsAlgorithm, on: $notificationsAlgorithm) { notificationsAlgorithm = $0 }
-
-            subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
-            subscribeSetting(\.glucoseNotificationsOption, on: $glucoseNotificationsOption) { glucoseNotificationsOption = $0 }
-            subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
-                addSourceInfoToGlucoseNotifications = $0 }
-
-            subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
-                lowGlucose = $0
-            }, map: {
-                let clampedValue = max(min($0, 400), 40)
-                return clampedValue
-            })
-
-            subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
-                highGlucose = $0
-            }, map: {
-                let clampedValue = max(min($0, 400), 40)
-                return clampedValue
-            })
-        }
-    }
-}
-
-extension GlucoseNotificationSettings.StateModel: SettingsObserver {
-    func settingsDidChange(_: TrioSettings) {
-        units = settingsManager.settings.units
-    }
-}

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

@@ -1,389 +0,0 @@
-import ActivityKit
-import Combine
-import SwiftUI
-import Swinject
-
-extension GlucoseNotificationSettings {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        @StateObject var state = StateModel()
-
-        @State private var shouldDisplayHint: Bool = false
-        @State var hintDetent = PresentationDetent.large
-        @State var selectedVerboseHint: AnyView?
-        @State var hintLabel: String?
-        @State private var decimalPlaceholder: Decimal = 0.0
-        @State private var booleanPlaceholder: Bool = false
-        @State private var displayPickerLowGlucose: Bool = false
-        @State private var displayPickerHighGlucose: Bool = false
-
-        private var glucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            if state.units == .mmolL {
-                formatter.maximumFractionDigits = 1
-            }
-            formatter.roundingMode = .halfUp
-            return formatter
-        }
-
-        private var carbsFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            return formatter
-        }
-
-        @Environment(\.colorScheme) var colorScheme
-        @Environment(AppState.self) var appState
-
-        var body: some View {
-            List {
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.notificationsPump,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Always Notify Pump")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Always Notify Pump"),
-                    miniHint: String(localized: "Always Notify Pump Warnings."),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: ON").bold()
-                        Text(
-                            "With iOS Trio Notifications enabled, you can let Trio display most Pump Notifications in iOS Notification Center as a Banner, List and on the Lock Screen. It allows you to refer to Trio Information at a glance and troubleshoot any informational issue. Set iOS Notifications Banner Style to Persistent to display banners in the app until dismissed."
-                        )
-                        Text("If iOS Trio Notifications is disabled, Trio will display these messages in-app as a banner only.")
-                        Text("An example of a Pump Warning is 'Pod Expiration Reminder'")
-                    },
-                    headerText: String(localized: "Trio Information Notifications")
-                )
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.notificationsCgm,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Always Notify CGM")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Always Notify CGM"),
-                    miniHint: String(localized: "Always Notify CGM Warnings."),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: ON").bold()
-                        Text(
-                            "With iOS Trio Notifications enabled, you can let Trio display most CGM Notifications in iOS Notification Center as a Banner, List and on the Lock Screen. It allows you to refer to Trio Information at a glance and troubleshoot any informational issue. Set iOS Notifications Banner Style to Persistent to display banners in the app until dismissed."
-                        )
-                        Text("If iOS Trio Notifications is disabled, Trio will display these messages in-app as a banner only.")
-                        Text("An example of a CGM Warning is 'Unable to open the app'")
-                    }
-                )
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.notificationsCarb,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Always Notify Carb")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Always Notify Carb"),
-                    miniHint: String(localized: "Always Notify Carb Warnings."),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: ON").bold()
-                        Text(
-                            "With iOS Trio Notifications enabled, you can let Trio display most Carb Notifications in iOS Notification Center as a Banner, List and on the Lock Screen. It allows you to refer to Trio Information at a glance and troubleshoot any informational issue. Set iOS Notifications Banner Style to Persistent to display banners in the app until dismissed."
-                        )
-                        Text("If iOS Trio Notifications is disabled, Trio will display these messages in-app as a banner only.")
-                        Text("An example of a Carb Warning is 'Carbs required: 30 g'")
-                    }
-                )
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.notificationsAlgorithm,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Always Notify Algorithm")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Always Notify Algorithm"),
-                    miniHint: String(localized: "Always Notify Algorithm Warnings."),
-                    verboseHint:
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: ON").bold()
-                        Text(
-                            "With iOS Trio Notifications enabled, you can let Trio display most Algorithm Notifications in iOS Notification Center as a Banner, List and on the Lock Screen. It allows you to refer to Trio Information at a glance and troubleshoot any informational issue. Set iOS Notifications Banner Style to Persistent to display banners in the app until dismissed."
-                        )
-                        Text("If iOS Trio Notifications is disabled, Trio will display these messages in-app as a banner only.")
-                        Text(
-                            "An example of an Algorithm Warning is 'Error: Invalid glucose: Not enough glucose data'"
-                        )
-                    }
-                )
-
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.glucoseBadge,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Show Glucose App Badge")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Show Glucose App Badge"),
-                    miniHint: String(localized: "Show your current glucose on Trio app icon."),
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text(
-                            "This will add your current glucose on the top right of your Trio icon as a red notification badge. Changing setting takes effect on next Glucose reading."
-                        )
-                    },
-                    headerText: String(localized: "Various Glucose Notifications")
-                )
-
-                Section {
-                    VStack {
-                        Picker(
-                            selection: $state.glucoseNotificationsOption,
-                            label: Text("Glucose Notifications")
-                        ) {
-                            ForEach(GlucoseNotificationsOption.allCases) { selection in
-                                Text(selection.displayName).tag(selection)
-                            }
-                        }.padding(.top)
-
-                        HStack(alignment: .center) {
-                            Text(
-                                "Choose glucose notifications option. See hint for more details."
-                            )
-                            .font(.footnote)
-                            .foregroundColor(.secondary)
-                            .lineLimit(nil)
-                            Spacer()
-                            Button(
-                                action: {
-                                    hintLabel = String(localized: "Glucose Notifications")
-                                    selectedVerboseHint =
-                                        AnyView(
-                                            VStack(alignment: .leading, spacing: 10) {
-                                                Text(
-                                                    "Set the Glucose Notifications Option. Descriptions for each option found below."
-                                                )
-                                                VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Disabled:").bold()
-                                                    Text("No Glucose Notifications will be triggered.")
-                                                }
-                                                VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Always:").bold()
-                                                    Text(
-                                                        "A notification will be triggered every time your glucose is updated in Trio."
-                                                    )
-                                                }
-                                                VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Only Alarm Limits:").bold()
-                                                    Text(
-                                                        "A notification will be triggered only when glucose levels are below the LOW limit or above the HIGH limit, as specified in Glucose Alarm Limits below."
-                                                    )
-                                                }
-                                            }
-                                        )
-                                    shouldDisplayHint.toggle()
-                                },
-                                label: {
-                                    HStack {
-                                        Image(systemName: "questionmark.circle")
-                                    }
-                                }
-                            ).buttonStyle(BorderlessButtonStyle())
-                        }.padding(.top)
-                    }.padding(.bottom)
-                }.listRowBackground(Color.chart)
-
-                if state.glucoseNotificationsOption != GlucoseNotificationsOption.disabled {
-                    self.lowAndHighGlucoseAlertSection
-                    SettingInputSection(
-                        decimalValue: $decimalPlaceholder,
-                        booleanValue: $state.addSourceInfoToGlucoseNotifications,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Add Glucose Source to Alarm")
-                            }
-                        ),
-                        units: state.units,
-                        type: .boolean,
-                        label: String(localized: "Add Glucose Source to Alarm"),
-                        miniHint: String(localized: "Source of the glucose reading will be added to the notification."),
-                        verboseHint: VStack(alignment: .leading, spacing: 10) {
-                            Text("Default: OFF").bold()
-                            Text("The source of the glucose reading will be added to the notification.")
-                        }
-                    )
-                }
-            }
-            .listSectionSpacing(sectionSpacing)
-            .sheet(isPresented: $shouldDisplayHint) {
-                SettingInputHintView(
-                    hintDetent: $hintDetent,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    hintLabel: hintLabel ?? "",
-                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
-                    sheetTitle: String(localized: "Help", comment: "Help sheet title")
-                )
-            }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .onAppear(perform: configureView)
-            .navigationBarTitle("Trio Notifications")
-            .navigationBarTitleDisplayMode(.automatic)
-            .settingsHighlightScroll()
-        }
-
-        var lowAndHighGlucoseAlertSection: some View {
-            Section {
-                VStack {
-                    VStack {
-                        HStack {
-                            Text("Low Glucose Alarm Limit")
-
-                            Spacer()
-
-                            Group {
-                                Text(
-                                    state.units == .mgdL ? state.lowGlucose.description : state.lowGlucose.formattedAsMmolL
-                                )
-                                .foregroundColor(!displayPickerLowGlucose ? .primary : .accentColor)
-
-                                Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
-                            }
-                        }
-                        .onTapGesture {
-                            displayPickerLowGlucose.toggle()
-                        }
-                    }
-                    .padding(.top)
-
-                    if displayPickerLowGlucose {
-                        let setting = PickerSettingsProvider.shared.settings.lowGlucose
-
-                        Picker(selection: $state.lowGlucose, label: Text("")) {
-                            ForEach(
-                                PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
-                                id: \.self
-                            ) { value in
-                                let displayValue = state.units == .mgdL ? value.description : value.formattedAsMmolL
-                                Text(displayValue).tag(value)
-                            }
-                        }
-                        .pickerStyle(WheelPickerStyle())
-                        .frame(maxWidth: .infinity)
-                    }
-
-                    VStack {
-                        HStack {
-                            Text("High Glucose Alarm Limit")
-
-                            Spacer()
-
-                            Group {
-                                Text(
-                                    state.units == .mgdL ? state.highGlucose.description : state.highGlucose.formattedAsMmolL
-                                )
-                                .foregroundColor(!displayPickerHighGlucose ? .primary : .accentColor)
-
-                                Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
-                            }
-                        }
-                        .onTapGesture {
-                            displayPickerHighGlucose.toggle()
-                        }
-                    }
-                    .padding(.top)
-
-                    if displayPickerHighGlucose {
-                        let setting = PickerSettingsProvider.shared.settings.highGlucose
-                        Picker(selection: $state.highGlucose, label: Text("")) {
-                            ForEach(
-                                PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
-                                id: \.self
-                            ) { value in
-                                let displayValue = state.units == .mgdL ? value.description : value.formattedAsMmolL
-                                Text(displayValue).tag(value)
-                            }
-                        }
-                        .pickerStyle(WheelPickerStyle())
-                        .frame(maxWidth: .infinity)
-                    }
-
-                    HStack(alignment: .center) {
-                        Text(
-                            "Sets the lower and upper limit for glucose alarms."
-                        )
-                        .lineLimit(nil)
-                        .font(.footnote)
-                        .foregroundColor(.secondary)
-                        Spacer()
-                        Button(
-                            action: {
-                                hintLabel = String(localized: "Low and High Glucose Alarm Limits")
-                                selectedVerboseHint =
-                                    AnyView(VStack(alignment: .leading, spacing: 10) {
-                                        let low: Decimal = 70
-                                        let high: Decimal = 180
-                                        let labelLow = (state.units == .mgdL ? low.description : low.formattedAsMmolL) + " " +
-                                            state.units.rawValue
-                                        let labelHigh = (state.units == .mgdL ? high.description : high.formattedAsMmolL) + " " +
-                                            state.units.rawValue
-                                        Text("Low Default: " + labelLow).bold()
-                                        Text("High Default: " + labelHigh).bold()
-                                        VStack(alignment: .leading, spacing: 10) {
-                                            Text(
-                                                "These two settings determine the range outside of which you will be notified via push notifications."
-                                            )
-                                            Text(
-                                                "If your CGM readings are below the Low value or above the High value, you will receive a glucose alarm."
-                                            )
-                                        }
-                                    })
-                                shouldDisplayHint.toggle()
-                            },
-                            label: {
-                                HStack {
-                                    Image(systemName: "questionmark.circle")
-                                }
-                            }
-                        ).buttonStyle(BorderlessButtonStyle())
-                    }.padding(.top)
-                }.padding(.bottom)
-            }.listRowBackground(Color.chart)
-        }
-    }
-}

+ 3 - 0
Trio/Sources/Modules/Home/HomeProvider.swift

@@ -13,6 +13,9 @@ extension Home {
         }
 
         func heartbeatNow() {
+            // User-initiated force-loop: bypass dwell suppression so transient
+            // errors surface immediately rather than waiting for count/dwell.
+            apsManager.markNextLoopUserInitiated()
             apsManager.heartbeat(date: Date())
         }
 

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -423,7 +423,7 @@ extension Home {
                 .map { [weak self] error in
                     self?.errorDate = error == nil ? nil : Date()
                     if let error = error {
-                        info(.default, String(describing: error), notificationText: error.localizedDescription)
+                        debug(.default, "APSManager lastError: \(String(describing: error))")
                     }
                     return error?.localizedDescription
                 }

+ 5 - 1
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -36,6 +36,7 @@ extension Home {
         @State var showQuickBolusNoHistory = false
         @State var showPumpSelection: Bool = false
         @State var showCGMSelection: Bool = false
+        @State var showSnoozeSheet: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
             TimePicker(active: false, hours: 4),
@@ -151,7 +152,7 @@ extension Home {
             .onLongPressGesture {
                 let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                 impactHeavy.impactOccurred()
-                state.showModal(for: .snooze)
+                showSnoozeSheet = true
             }
         }
 
@@ -997,6 +998,9 @@ extension Home {
             .sheet(isPresented: $state.isLegendPresented) {
                 ChartLegendView(state: state)
             }
+            .sheet(isPresented: $showSnoozeSheet) {
+                SnoozeAlertsSheetView(resolver: resolver, isPresented: $showSnoozeSheet)
+            }
             // PUMP RELATED
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }

+ 2 - 2
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift

@@ -220,9 +220,9 @@ struct LiveActivityWidgetConfiguration: BaseView {
                 let pointMarkColor = Trio.getDynamicGlucoseColor(
                     glucoseValue: Decimal(data.glucoseLevel),
                     highGlucoseColorValue: !(state.settingsManager.settings.glucoseColorScheme == .dynamicColor) ? state
-                        .settingsManager.settings.highGlucose : Decimal(220),
+                        .settingsManager.settings.high : Decimal(220),
                     lowGlucoseColorValue: !(state.settingsManager.settings.glucoseColorScheme == .dynamicColor) ? state
-                        .settingsManager.settings.lowGlucose : Decimal(55),
+                        .settingsManager.settings.low : Decimal(55),
                     targetGlucose: Decimal(100),
                     glucoseColorScheme: state.settingsManager.settings.glucoseColorScheme
                 )

+ 0 - 307
Trio/Sources/Modules/Main/MainStateModel.swift

@@ -1,6 +1,5 @@
 import Combine
 import LoopKitUI
-import SwiftMessages
 import SwiftUI
 import Swinject
 
@@ -12,237 +11,6 @@ extension Main {
         @Published var modal: Modal?
         @Published var secondaryModal: SecondaryModalWrapper?
 
-        @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
-        private var timers: [TimeInterval: Timer] = [:]
-
-        private func showTriggeredView(
-            message: MessageContent,
-            interval _: TimeInterval,
-            config: SwiftMessages.Config,
-            view: MessageView
-        ) {
-            view.customConfigureTheme(
-                colorSchemePreference: colorSchemePreference
-            )
-            setupAction(message: message, view: view)
-
-            SwiftMessages.show(config: config, view: view)
-        }
-
-        // Add or replace timer for a specific TimeInterval
-        private func addOrReplaceTriggerTimer(message: MessageContent, config: SwiftMessages.Config, view: MessageView) {
-            let trigger = message.trigger as! UNTimeIntervalNotificationTrigger
-            guard trigger.timeInterval > 0 else { return }
-            let interval = trigger.timeInterval
-
-            SwiftMessages.hide(id: view.id)
-
-            // If a timer already exists for this interval, invalidate it
-            if let existingTimer = timers[interval] {
-                existingTimer.invalidate()
-            }
-
-            // Create a new timer with the provided interval
-            let newTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
-                self?.showTriggeredView(message: message, interval: interval, config: config, view: view)
-                self?.timers[interval] = nil
-            }
-
-            timers[interval] = newTimer
-        }
-
-        // Cancel all timers (optional cleanup method)
-        private func cancelAllTimers() {
-            timers.values.forEach { $0.invalidate() }
-            timers.removeAll()
-        }
-
-        private func setupPumpConfig() {
-            // display the pump configuration immediatly
-            if let pump = provider.deviceManager.pumpManager,
-               let bluetooth = provider.bluetoothProvider
-            {
-                let view = PumpConfig.PumpSettingsView(
-                    pumpManager: pump,
-                    bluetoothManager: bluetooth,
-                    completionDelegate: self
-                ).asAny()
-                router.mainSecondaryModalView.send(view)
-            }
-        }
-
-        private func setupButton(message _: MessageContent, view: MessageView) {
-            view.button?.setImage(UIImage(), for: .normal)
-            view.iconLabel = nil
-            let buttonImage = UIImage(systemName: "chevron.right")?.withTintColor(.white)
-            view.button?.setImage(buttonImage, for: .normal)
-            view.button?.backgroundColor = view.backgroundView.backgroundColor
-            view.button?.tintColor = view.iconImageView?.tintColor
-        }
-
-        private func setupAction(message: MessageContent, view: MessageView) {
-            switch message.action {
-            case .snooze:
-                setupButton(message: message, view: view)
-                view.buttonTapHandler = { _ in
-                    // Popup Snooze view when user taps on Glucose Notification
-                    SwiftMessages.hide()
-                    self.router.mainModalScreen.send(.snooze)
-                }
-            case .pumpConfig:
-                setupButton(message: message, view: view)
-                view.buttonTapHandler = { _ in
-                    SwiftMessages.hide()
-                    self.setupPumpConfig()
-                }
-            default: // break
-                view.button?.setImage(UIImage(), for: .normal)
-                view.buttonTapHandler = { _ in
-                    SwiftMessages.hide()
-                }
-            }
-        }
-
-        private func isApnPumpConfigAction(_ message: MessageContent) -> Bool {
-            if message.type != .error, message.action == .pumpConfig {
-                setupPumpConfig()
-                return true
-            }
-            return false
-        }
-
-        private func showAlertMessage(_ message: MessageContent) {
-            if message.useAPN, !alertPermissionsChecker.notificationsDisabled
-            {
-                showAPN(message)
-            } else {
-                showSwiftMessage(message)
-            }
-        }
-
-        private func showAPN(_ message: MessageContent) {
-            DispatchQueue.main.async {
-                self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
-                    $0.alertMessageNotification(message)
-                }
-            }
-        }
-
-        // Read the color scheme preference from UserDefaults; defaults to system default setting
-        @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
-
-        private func showSwiftMessage(_ message: MessageContent) {
-            if snoozeUntilDate > Date(), message.action == .snooze {
-                return
-            }
-
-            var config = SwiftMessages.defaultConfig
-            let view = MessageView.viewFromNib(layout: .cardView)
-
-            view.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
-            config.prefersStatusBarHidden = true
-
-            // Set id so that multiple notifications are not queued while waiting for user response; only the latest will be shown
-            if message.subtype == .glucose || message.subtype == .carb {
-                view.id = message.type.rawValue + message.subtype.rawValue
-            }
-
-            let titleContent: String
-
-            let iconName = UIApplication.shared.alternateIconName ?? "trioBlack"
-            let iconImage = UIImage(named: iconName) ?? UIImage()
-
-            view.configureContent(
-                title: "title",
-                body: message.content,
-                iconImage: nil,
-                iconText: nil,
-                buttonImage: nil,
-                buttonTitle: nil,
-                buttonTapHandler: nil
-            )
-
-            view.configureIcon(withSize: CGSize(width: 40, height: 40), contentMode: .scaleAspectFit)
-            view.iconImageView!.image = iconImage
-            view.iconImageView?.layer.cornerRadius = 10
-
-            view.customConfigureTheme(
-                colorSchemePreference: colorSchemePreference
-            )
-
-            view.iconImageView?.image = iconImage
-
-            switch message.type {
-            case .info,
-                 .other:
-                config.duration = .seconds(seconds: 5)
-                titleContent = message.title != "" ? message.title : String(localized: "Info", comment: "Info title")
-            case .warning:
-                config.duration = .forever
-                titleContent = message.title != "" ? message
-                    .title : String(localized: "Warning", comment: "Warning title")
-            case .error:
-                config.duration = .forever
-                titleContent = message.title != "" ? message
-                    .title : String(localized: "Error", comment: "Error title")
-            }
-
-            view.titleLabel?.text = titleContent
-            config.dimMode = .gray(interactive: true)
-
-            setupAction(message: message, view: view)
-            if message.trigger != nil {
-                addOrReplaceTriggerTimer(message: message, config: config, view: view)
-            }
-
-            guard message.type == .error || message.action != .pumpConfig, message.trigger == nil, !view.isHidden else { return }
-
-            SwiftMessages.show(config: config, view: view)
-        }
-
-        /*
-          Reclassification is needed for Medtronic pumps for 'Pump error:' RileyLink related messages.
-          For details, see https://discord.com/channels/1020905149037813862/1338245444186279946/1343469793013141525.
-          These messages are repeatedly displayed causing users to simply ignore them.
-          Reclassification of these Info type messages is based on APSManager.APSError enum values.
-          We reclassify APSError.pumpError and APSError.invalidPumpState as MessageType.info and MessageSubtype.pump.
-          This allows the user to disable these messages using using the 'Trio Notification' -> 'Always Notify Pump' setting.
-          MessageType.error messagges are always displayed to the user and the user cannot disable them.
-          Other APSManager.APSError remain as MessageType.info which allows users to disable them
-          using the 'Trio Notification' -> 'Always Notify Algorithm' setting.
-         */
-
-        func reclassifyInfoNotification(_ message: inout MessageContent) {
-            if message.title == "" {
-                switch message.type {
-                case .info:
-                    if message.content.range(of: "error", options: .caseInsensitive) != nil || message.content
-                        .range(of: String(localized: "Error"), options: .caseInsensitive) != nil
-                    {
-                        message.title = String(localized: "Error", comment: "Error title")
-                    } else {
-                        message.title = String(localized: "Info", comment: "Info title")
-                    }
-                    if APSError.pumpWarningMatches(message: message.content) {
-                        message.subtype = .pump
-                        let lastLoopMinutes = Int((Date().timeIntervalSince(apsManager.lastLoopDate) - 30) / 60) + 1
-                        if lastLoopMinutes > 10 {
-                            message.type = .error
-                        }
-                    } else if APSError.pumpErrorMatches(message: message.content) {
-                        message.subtype = .pump
-                        message.type = .error
-                    }
-                case .warning:
-                    message.title = String(localized: "Warning", comment: "Warning title")
-                case .error:
-                    message.title = String(localized: "Error", comment: "Error title")
-                case .other:
-                    message.title = String(localized: "Info", comment: "Info title")
-                }
-            }
-        }
-
         override func subscribe() {
             router.mainModalScreen
                 .map { $0?.modal(resolver: self.resolver!) }
@@ -258,17 +26,6 @@ extension Main {
                 }
                 .store(in: &lifetime)
 
-            router.alertMessage
-                .receive(on: DispatchQueue.main)
-                .sink { message in
-                    guard !self.isApnPumpConfigAction(message) else { return }
-                    var reclassifyMessage = message
-                    self.reclassifyInfoNotification(&reclassifyMessage)
-                    guard self.router.allowNotify(reclassifyMessage, self.settingsManager.settings) else { return }
-                    self.showAlertMessage(reclassifyMessage)
-                }
-                .store(in: &lifetime)
-
             router.mainSecondaryModalView
                 .receive(on: DispatchQueue.main)
                 .sink { view in
@@ -286,67 +43,3 @@ extension Main {
         }
     }
 }
-
-extension MessageView {
-    func currentColorScheme() -> ColorScheme {
-        let userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
-        return userInterfaceStyle == .dark ? .dark : .light
-    }
-
-    func customConfigureTheme(colorSchemePreference: ColorSchemeOption) {
-        let defaultSystemColorScheme = currentColorScheme()
-        var backgroundColor = UIColor.systemBackground
-        var foregroundColor = UIColor.white
-        let ApnBackground = UIColor(named: "ApnBackground") ?? UIColor.lightGray
-        let iOSlightTrioDark = UIColor(named: "ApnBackgroundLightDark") ?? UIColor.lightGray
-
-        switch colorSchemePreference {
-        case .systemDefault:
-            backgroundColor = ApnBackground
-            foregroundColor = UIColor.label
-        case .dark:
-            backgroundColor = defaultSystemColorScheme == .light ? iOSlightTrioDark : ApnBackground
-            foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
-        case .light:
-            backgroundColor = defaultSystemColorScheme == .light ? ApnBackground : UIColor.gray
-            foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
-        }
-
-        iconImageView?.tintColor = foregroundColor
-        backgroundView.backgroundColor = backgroundColor
-        titleLabel?.textColor = foregroundColor
-        bodyLabel?.textColor = foregroundColor
-        iconImageView?.isHidden = iconImageView?.image == nil
-
-        backgroundView.layer.cornerRadius = 25
-
-        let adjustedFont = UIFont.systemFont(ofSize: 13.0, weight: .bold)
-        let preferredTitleFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: adjustedFont)
-        let preferredBodyFont = UIFont.preferredFontforStyle(forTextStyle: .footnote)
-        // Set the title and body font to the dynamic type sizes
-        titleLabel?.adjustsFontForContentSizeCategory = true
-        titleLabel?.font = preferredTitleFont
-        bodyLabel?.adjustsFontForContentSizeCategory = true
-        bodyLabel?.font = preferredBodyFont
-        // Set custom colors for title and body text
-        titleLabel?.textColor = foregroundColor
-        bodyLabel?.textColor = foregroundColor
-    }
-}
-
-@available(iOS 16.0, *)
-extension Main.StateModel: CompletionDelegate {
-    func completionNotifyingDidComplete(_: CompletionNotifying) {
-        // close the window
-        router.mainSecondaryModalView.send(nil)
-    }
-}
-
-// Extension to convert SwiftUI TextStyle to UIFont
-extension UIFont {
-    static func preferredFontforStyle(forTextStyle: UIFont.TextStyle) -> UIFont {
-        let uiFontMetrics = UIFontMetrics.default
-        let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: forTextStyle)
-        return uiFontMetrics.scaledFont(for: UIFont(descriptor: descriptor, size: 0))
-    }
-}

+ 5 - 1
Trio/Sources/Modules/Main/View/MainRootView.swift

@@ -9,6 +9,10 @@ extension Main {
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
+        private var modalScheduler: TrioModalAlertScheduler {
+            resolver.resolve(TrioAlertManager.self)!.modalScheduler
+        }
+
         var body: some View {
             router.view(for: .home)
                 .sheet(item: $state.modal) { modal in
@@ -18,7 +22,7 @@ extension Main {
                 .sheet(item: $state.secondaryModal) { wrapper in
                     wrapper.view
                 }
-
+                .trioAlerts(modalScheduler)
                 .onAppear(perform: configureView)
                 .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
         }

+ 0 - 2
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -732,8 +732,6 @@ extension Onboarding {
             if !isFreshTrioInstall {
                 let providedSettings = settingsProvider.settings
 
-                settingsCopy.lowGlucose = settingsCopy.lowGlucose.clamp(to: providedSettings.lowGlucose)
-                settingsCopy.highGlucose = settingsCopy.highGlucose.clamp(to: providedSettings.highGlucose)
                 settingsCopy.carbsRequiredThreshold = settingsCopy.carbsRequiredThreshold
                     .clamp(to: providedSettings.carbsRequiredThreshold)
                 settingsCopy.individualAdjustmentFactor = settingsCopy.individualAdjustmentFactor

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -553,7 +553,7 @@ enum PumpOptionForOnboardingUnits: String, Equatable, CaseIterable, Identifiable
         case .minimed:
             return "Medtronic"
         case .omni:
-            return "All Omnipod Types"
+            return "Omnipod"
         case .dana:
             return "Dana (RS/-i)"
         case .medtrum:

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

@@ -5,12 +5,24 @@ import LoopKitUI
 enum PumpConfig {
     enum Config {}
 
-    enum PumpType: Equatable {
+    enum PumpType: Equatable, CaseIterable {
         case minimed
         case omni
         case dana
         case medtrum
         case simulator
+
+        /// `Alert.Identifier.managerIdentifier` emitted by this plugin's
+        /// `PumpManager`. Nil for `.simulator` — no real plugin.
+        var pluginIdentifier: String? {
+            switch self {
+            case .minimed: return "Minimed"
+            case .omni: return "Omni"
+            case .dana: return "Dana"
+            case .medtrum: return "Medtrum"
+            case .simulator: return nil
+            }
+        }
     }
 
     struct PumpInitialSettings {

+ 3 - 1
Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift

@@ -1,3 +1,4 @@
+import Foundation
 import LoopKit
 import LoopKitUI
 import SwiftDate
@@ -11,6 +12,7 @@ extension PumpConfig {
         private(set) var initialSettings: PumpInitialSettings = .default
         @Published var hasUnacknowledgedAlert: Bool = false
         @Injected() var bluetoothManager: BluetoothStateManager!
+        @Injected() var trioAlertManager: TrioAlertManager!
 
         override func subscribe() {
             provider.pumpDisplayState
@@ -49,7 +51,7 @@ extension PumpConfig {
         }
 
         func ack() {
-            provider.deviceManager.alertHistoryStorage.broadcastAlertUpdates()
+            trioAlertManager.acknowledgeAllOutstanding()
         }
     }
 }

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

@@ -48,6 +48,7 @@ extension PumpConfig {
                                     Spacer()
                                     Button("Acknowledge all alerts") { state.ack() }
                                 }
+                                Spacer()
                             } else {
                                 VStack {
                                     Button {

+ 40 - 15
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -328,24 +328,49 @@ enum SettingItems {
     static let notificationItems = [
         SettingItem(title: "Manage iOS Preferences", view: .notificationSettings),
         SettingItem(
-            title: "Trio Notifications",
-            view: .glucoseNotificationSettings,
+            title: "Glucose Alarms",
+            view: .glucoseAlerts,
             searchContents: [
-                "Always Notify Pump",
-                "Always Notify CGM",
-                "Always Notify Carb",
-                "Always Notify Algorithm",
-                "Show Glucose App Badge",
-                "Glucose Notifications",
-                "Add Glucose Source to Alarm",
-                "Low Glucose Alarm Limit",
-                "High Glucose Alarm Limit"
+                "Urgent Low Glucose",
+                "Low Glucose",
+                "Forecasted Low Glucose",
+                "High Glucose",
+                "Day & Night",
+                "Alert Sound",
+                "Override Silence",
+                "Show Glucose App Badge"
             ],
-            scrollTargetLabels: [
-                "Low Glucose Alarm Limit": "Glucose Notifications",
-                "High Glucose Alarm Limit": "Glucose Notifications"
+            path: ["Notifications", "Glucose Alarms"]
+        ),
+        SettingItem(
+            title: "Device Alarms",
+            view: .deviceAlarms,
+            searchContents: [
+                "Critical",
+                "Time-Sensitive",
+                "Normal",
+                "Occlusion",
+                "Pump Fault",
+                "Hardware Fault",
+                "Reservoir",
+                "Battery",
+                "Bolus Failed",
+                "Pod",
+                "Sensor Failure",
+                "Sensor Stopped",
+                "Calibration",
+                "Signal Loss",
+                "Device Expired",
+                "Not Looping",
+                "Algorithm"
             ],
-            path: ["Notifications", "Trio Notifications"] // Glucose
+            path: ["Notifications", "Device Alarms"]
+        ),
+        SettingItem(
+            title: "Day & Night Windows",
+            view: .alarmWindows,
+            searchContents: ["Day Starts", "Night Starts", "Day & Night"],
+            path: ["Notifications", "Day & Night Windows"]
         ),
         SettingItem(
             title: "Live Activity",

+ 38 - 36
Trio/Sources/Modules/Settings/View/Subviews/NotificationsView.swift

@@ -15,6 +15,7 @@ struct NotificationsView: BaseView {
     @ObservedObject var state: Settings.StateModel
     @State var notificationsDisabled = false
     @State var showAlert = false
+    @State private var showSnoozeSheet = false
     @State private var shouldDisplayHint: Bool = false
     @State var hintDetent = PresentationDetent.large
     @State var selectedVerboseHint: AnyView? = AnyView(
@@ -33,52 +34,50 @@ struct NotificationsView: BaseView {
     var body: some View {
         List {
             Section(
-                header: Text("Manage iOS Preferences"),
+                header: Text("Mute Trio Alarms"),
                 content: {
-                    manageNotifications
+                    VStack {
+                        Button {
+                            showSnoozeSheet = true
+                        } label: {
+                            HStack {
+                                Image(systemName: "zzz").foregroundStyle(.tint)
+                                Text("Snooze All Alarms").bold()
+                            }
+                            .font(.title3)
+                        }
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .buttonStyle(.bordered)
+                    }.padding(.vertical)
                 }
             ).listRowBackground(Color.chart)
 
-            Section {
-                VStack {
+            Section(
+                header: Text("Manage iOS Preferences"),
+                content: {
                     notificationsEnabledStatus
-                    HStack(alignment: .top) {
-                        Text("Notifications give you important Trio information without requiring you to open the app.")
-                            .font(.footnote)
-                            .foregroundColor(.secondary)
-                            .lineLimit(nil)
-                        Spacer()
-                        Button(
-                            action: {
-                                hintLabel = String(localized: "Manage iOS Preferences")
-                                selectedVerboseHint = AnyView(
-                                    VStack(alignment: .leading, spacing: 10) {
-                                        Text(
-                                            "Notifications give you important Trio information without requiring you to open the app."
-                                        )
-                                        Text(
-                                            "Keep these turned ON in your phone’s settings to ensure you receive Trio Notifications, Critical Alerts, and Time Sensitive Notifications."
-                                        )
-                                    }
-                                )
-                                shouldDisplayHint.toggle()
-                            },
-                            label: {
-                                HStack {
-                                    Image(systemName: "questionmark.circle")
-                                }
-                            }
-                        ).buttonStyle(BorderlessButtonStyle())
-                    }.padding(.top)
-                }.padding(.bottom)
-            }.listRowBackground(Color.chart)
+
+                    manageNotifications
+                }
+            ).listRowBackground(Color.chart)
 
             Section(
                 header: Text("Notification Center"),
                 content: {
-                    Text("Trio Notifications")
-                        .navigationLink(to: .glucoseNotificationSettings, from: self)
+                    Text("Glucose Alarms")
+                        .navigationLink(to: .glucoseAlerts, from: self)
+
+                    Text("Device Alarms")
+                        .navigationLink(to: .deviceAlarms, from: self)
 
+                    Text("Day & Night Windows")
+                        .navigationLink(to: .alarmWindows, from: self)
+                }
+            ).listRowBackground(Color.chart)
+
+            Section(
+                header: Text("Other Notifications"),
+                content: {
                     if #available(iOS 16.2, *) {
                         Text("Live Activity").navigationLink(to: .liveActivitySettings, from: self)
                     }
@@ -112,6 +111,9 @@ struct NotificationsView: BaseView {
                 sheetTitle: String(localized: "Help", comment: "Help sheet title")
             )
         }
+        .sheet(isPresented: $showSnoozeSheet) {
+            SnoozeAlertsSheetView(resolver: resolver, isPresented: $showSnoozeSheet)
+        }
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))
         .navigationTitle("Notifications")

+ 0 - 55
Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift

@@ -853,64 +853,9 @@ extension SettingsExport {
                 addSetting(
                     category: notificationsCategory,
                     subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Always Notify Pump"),
-                    value: trioSettings.notificationsPump ? String(localized: "Enabled") : String(localized: "Disabled")
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Always Notify CGM"),
-                    value: trioSettings.notificationsCgm ? String(localized: "Enabled") : String(localized: "Disabled")
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Always Notify Carb"),
-                    value: trioSettings.notificationsCarb ? String(localized: "Enabled") : String(localized: "Disabled")
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Always Notify Algorithm"),
-                    value: trioSettings.notificationsAlgorithm ? String(localized: "Enabled") : String(localized: "Disabled")
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
                     name: String(localized: "Show Glucose App Badge"),
                     value: trioSettings.glucoseBadge ? String(localized: "Enabled") : String(localized: "Disabled")
                 )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Glucose Notifications"),
-                    value: trioSettings.glucoseNotificationsOption.rawValue
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Add Glucose Source to Alarm"),
-                    value: trioSettings
-                        .addSourceInfoToGlucoseNotifications ? String(localized: "Enabled") : String(localized: "Disabled")
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "Low Glucose Alarm Limit"),
-                    value: trioSettings
-                        .units == .mgdL ? String(describing: trioSettings.lowGlucose) :
-                        String(describing: trioSettings.lowGlucose.asMmolL),
-                    unit: trioSettings.units.rawValue
-                )
-                addSetting(
-                    category: notificationsCategory,
-                    subcategory: trioNotificationsSubcategory,
-                    name: String(localized: "High Glucose Alarm Limit"),
-                    value: trioSettings
-                        .units == .mgdL ? String(describing: trioSettings.highGlucose) :
-                        String(describing: trioSettings.highGlucose.asMmolL),
-                    unit: trioSettings.units.rawValue
-                )
 
                 // Live Activity subcategory
                 let liveActivitySubcategory = String(localized: "Live Activity")

+ 0 - 5
Trio/Sources/Modules/Snooze/SnoozeDataFlow.swift

@@ -1,5 +0,0 @@
-enum Snooze {
-    enum Config {}
-}
-
-protocol SnoozeProvider {}

+ 0 - 3
Trio/Sources/Modules/Snooze/SnoozeProvider.swift

@@ -1,3 +0,0 @@
-extension Snooze {
-    final class Provider: BaseProvider, SnoozeProvider {}
-}

+ 0 - 44
Trio/Sources/Modules/Snooze/SnoozeStateModel.swift

@@ -1,44 +0,0 @@
-import Observation
-import SwiftUI
-
-extension Snooze {
-    @Observable final class StateModel: BaseStateModel<Provider> {
-        @ObservationIgnored @Persisted(key: "UserNotificationsManager.snoozeUntilDate") var snoozeUntilDate: Date = .distantPast
-        @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
-        @ObservationIgnored @Injected() var notificationsManager: UserNotificationsManager!
-        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
-
-        var alarm: GlucoseAlarm?
-
-        override func subscribe() {
-            alarm = glucoseStorage.alarm
-            broadcaster.register(SnoozeObserver.self, observer: self)
-        }
-
-        func unsubscribe() {
-            broadcaster.unregister(SnoozeObserver.self, observer: self)
-        }
-
-        // Add validation helper inside the class
-        private func validateSnoozeDuration(_ duration: TimeInterval) -> Bool {
-            // Only allow durations matching our defined actions
-            NotificationResponseAction.allCases
-                .map(\.duration)
-                .contains(duration)
-        }
-
-        @MainActor func applySnooze(_ duration: TimeInterval) async {
-            // Allow any duration chosen in the Snooze UI, while keeping validation for quick actions elsewhere.
-            snoozeUntilDate = duration > 0 ? Date().addingTimeInterval(duration) : .distantPast
-            alarm = glucoseStorage.alarm
-            await notificationsManager.applySnooze(for: duration)
-        }
-    }
-}
-
-extension Snooze.StateModel: SnoozeObserver {
-    func snoozeDidChange(_ untilDate: Date) {
-        snoozeUntilDate = untilDate
-        alarm = glucoseStorage.alarm
-    }
-}

+ 0 - 158
Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift

@@ -1,158 +0,0 @@
-import AudioToolbox
-import SwiftUI
-import Swinject
-
-extension Snooze {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        @State var state = StateModel()
-
-        @Environment(\.colorScheme) var colorScheme
-        @Environment(AppState.self) var appState
-
-        @State private var selectedInterval = 0
-        @State private var snoozeDescription = "nothing to see here"
-
-        private var pickerTimes: [TimeInterval] {
-            [
-                TimeInterval(minutes: 20), // 20 minutes
-                TimeInterval(hours: 1), // 1 hour
-                TimeInterval(hours: 3), // 3 hours
-                TimeInterval(hours: 6) // 6 hours
-            ]
-        }
-
-        private var formatter: DateComponentsFormatter {
-            let formatter = DateComponentsFormatter()
-            formatter.allowsFractionalUnits = false
-            formatter.unitsStyle = .full
-            return formatter
-        }
-
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeStyle = .short
-            return formatter
-        }
-
-        private func formatInterval(_ interval: TimeInterval) -> String {
-            formatter.string(from: interval) ?? ""
-        }
-
-        func getSnoozeDescription() -> String {
-            var snoozeDescription = ""
-            var celltext = ""
-
-            switch state.alarm {
-            case .high:
-                celltext = String(localized: "High Glucose Alarm active", comment: "High Glucose Alarm active")
-            case .low:
-                celltext = String(localized: "Low Glucose Alarm active", comment: "Low Glucose Alarm active")
-            case .none:
-                celltext = String(localized: "No Glucose Alarm active", comment: "No Glucose Alarm active")
-            }
-
-            if state.snoozeUntilDate > Date() {
-                snoozeDescription = String(
-                    format: String(localized: "snoozing until %@", comment: "snoozing until %@"),
-                    dateFormatter.string(from: state.snoozeUntilDate)
-                )
-            } else {
-                snoozeDescription = String(localized: "not snoozing", comment: "not snoozing")
-            }
-
-            return [celltext, snoozeDescription].joined(separator: ", ")
-        }
-
-        private var snoozeButton: some View {
-            VStack(alignment: .leading) {
-                Button {
-                    let interval = pickerTimes[selectedInterval]
-                    let snoozeFor = formatInterval(interval)
-                    let untilDate = Date() + interval
-
-                    Task { @MainActor [weak state] in
-                        guard let state = state else { return }
-                        await state.applySnooze(interval)
-                        debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))")
-                        snoozeDescription = getSnoozeDescription()
-                        state.hideModal()
-                    }
-                } label: {
-                    Text("Click to Snooze Alerts")
-                        .padding()
-                }
-            }
-        }
-
-        private var snoozePicker: some View {
-            VStack {
-                Picker(selection: $selectedInterval, label: Text("Strength")) {
-                    ForEach(0 ..< pickerTimes.count) {
-                        Text(formatInterval(self.pickerTimes[$0]))
-                    }
-                }
-                .pickerStyle(.wheel)
-            }
-        }
-
-        var body: some View {
-            Form {
-                Section {
-                    Text(snoozeDescription).lineLimit(nil)
-                    snoozePicker
-                    snoozeButton
-                }
-            }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationBarTitle("Snooze Alerts")
-            .navigationBarTitleDisplayMode(.automatic)
-            .toolbar {
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button("Close") {
-                        state.hideModal()
-                    }
-                }
-            }
-            .onAppear {
-                configureView()
-                snoozeDescription = getSnoozeDescription()
-            }
-            .onDisappear {
-                state.unsubscribe()
-            }
-        }
-    }
-}
-
-extension TimeInterval {
-    static func seconds(_ seconds: Double) -> TimeInterval {
-        seconds
-    }
-
-    static func minutes(_ minutes: Double) -> TimeInterval {
-        TimeInterval(minutes: minutes)
-    }
-
-    static func hours(_ hours: Double) -> TimeInterval {
-        TimeInterval(hours: hours)
-    }
-
-    init(minutes: Double) {
-        // self.init(minutes * 60)
-        let m = minutes * 60
-        self.init(m)
-    }
-
-    init(hours: Double) {
-        self.init(minutes: hours * 60)
-    }
-
-    var minutes: Double {
-        self / 60.0
-    }
-
-    var hours: Double {
-        minutes / 60.0
-    }
-}

+ 5 - 2
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -286,7 +286,7 @@ extension UserInterfaceSettings {
 
                             HStack(alignment: .center) {
                                 Text(
-                                    "Set low and high glucose values for the main screen, watch app and live activity glucose graph."
+                                    "Set low and high glucose values for the main screen, watch app, live activity, and contact image colors."
                                 )
                                 .lineLimit(nil)
                                 .font(.footnote)
@@ -303,7 +303,10 @@ extension UserInterfaceSettings {
                                                         "Default values are based on internationally accepted Time in Range values of \(state.units == .mgdL ? "70" : 70.formattedAsMmolL)-\(state.units == .mgdL ? "180" : 180.formattedAsMmolL) \(state.units.rawValue)."
                                                     ).bold()
                                                     Text(
-                                                        "Adjust these values if you would like the statistics to reflect different values than the internationally accepted Time In Range values used as the default."
+                                                        "These thresholds drive the chart band colors, statistics in-range buckets, Live Activity colors, and Contact Image colors."
+                                                    )
+                                                    Text(
+                                                        "To configure when alarms fire (urgent low / low / high / forecasted low), open Glucose Alarms."
                                                     )
                                                     Text("Note: These values are not used to calculate insulin dosing.")
                                                 }

+ 0 - 52
Trio/Sources/Router/Router.swift

@@ -2,44 +2,15 @@ import Combine
 import SwiftUI
 import Swinject
 
-enum MessageType: String {
-    case info
-    case warning
-    case error
-    case other
-}
-
-enum MessageSubtype: String {
-    case pump
-    case cgm
-    case carb
-    case glucose
-    case algorithm
-    case misc
-}
-
-struct MessageContent {
-    var content: String
-    var type: MessageType = .info
-    var subtype: MessageSubtype = .algorithm
-    var title: String = ""
-    var useAPN: Bool = true
-    var trigger: UNNotificationTrigger? = nil
-    var action: NotificationAction = .none
-}
-
 protocol Router {
     var mainModalScreen: CurrentValueSubject<Screen?, Never> { get }
     var mainSecondaryModalView: CurrentValueSubject<AnyView?, Never> { get }
-    var alertMessage: PassthroughSubject<MessageContent, Never> { get }
     func view(for screen: Screen) -> AnyView
-    func allowNotify(_ message: MessageContent, _ settings: TrioSettings) -> Bool
 }
 
 final class BaseRouter: Router {
     let mainModalScreen = CurrentValueSubject<Screen?, Never>(nil)
     let mainSecondaryModalView = CurrentValueSubject<AnyView?, Never>(nil)
-    let alertMessage = PassthroughSubject<MessageContent, Never>()
     private let resolver: Resolver
 
     init(resolver: Resolver) {
@@ -49,27 +20,4 @@ final class BaseRouter: Router {
     func view(for screen: Screen) -> AnyView {
         screen.view(resolver: resolver).asAny()
     }
-
-    func allowNotify(_ message: MessageContent, _ settings: TrioSettings) -> Bool {
-        if message.type == .error { return true }
-        switch message.subtype {
-        case .pump:
-            guard settings.notificationsPump else { return false }
-        case .cgm:
-            guard settings.notificationsCgm else { return false }
-        case .carb:
-            guard settings.notificationsCarb else { return false }
-        case .glucose:
-            guard (
-                message.type == .warning &&
-                    settings.glucoseNotificationsOption == GlucoseNotificationsOption.onlyAlarmLimits
-            ) ||
-                settings.glucoseNotificationsOption == GlucoseNotificationsOption.alwaysEveryCGM else { return false }
-        case .algorithm:
-            guard settings.notificationsAlgorithm else { return false }
-        case .misc:
-            return true
-        }
-        return true
-    }
 }

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

@@ -20,11 +20,12 @@ enum Screen: Identifiable, Hashable {
     case history
     case cgm
     case healthkit
-    case glucoseNotificationSettings
+    case glucoseAlerts
+    case deviceAlarms
+    case alarmWindows
     case mealSettings
     case iconConfig
     case overrideConfig
-    case snooze
     case statistics
     case watch
     case userInterfaceSettings
@@ -107,16 +108,18 @@ extension Screen {
             )
         case .healthkit:
             AppleHealthKit.RootView(resolver: resolver)
-        case .glucoseNotificationSettings:
-            GlucoseNotificationSettings.RootView(resolver: resolver)
+        case .glucoseAlerts:
+            GlucoseAlerts.RootView(resolver: resolver)
+        case .deviceAlarms:
+            DeviceAlarms.RootView(resolver: resolver)
+        case .alarmWindows:
+            AlarmWindows.RootView(resolver: resolver)
         case .mealSettings:
             MealSettings.RootView(resolver: resolver)
         case .iconConfig:
             IconConfig.RootView(resolver: resolver)
         case .overrideConfig:
             Adjustments.RootView(resolver: resolver)
-        case .snooze:
-            Snooze.RootView(resolver: resolver)
         case .watch:
             WatchConfig.RootView(resolver: resolver)
         case .statistics:

+ 259 - 0
Trio/Sources/Services/Alerts/AlertCatalogRegistry.swift

@@ -0,0 +1,259 @@
+import LoopKit
+
+/// Trio-side bridge of the upstream `AlertCatalogVendor` protocol. Pump
+/// plugins don't currently set `interruptionLevel` on the alerts they issue
+/// — every emission lands at LoopKit's default (`.timeSensitive`). Trio
+/// overrides the level by looking up the alert's identifier in this
+/// registry. As plugins adopt `AlertCatalogVendor` upstream, their entries
+/// move out of here and into the plugin repo.
+///
+/// Scope covers pump + CGM-issued alerts (LibreLoop sensor expiry / attention
+/// / reconnect). G5/G6/One/G7/One+ don't go through `issueAlert` — their
+/// status surfaces via `cgmStatusHighlight` instead. Trio-internal alerts
+/// (glucose thresholds, algorithm error, not-looping) set their level at
+/// construction and don't need a catalog entry.
+enum AlertCatalogRegistry {
+    static let entries: [Alert.CatalogEntry] =
+        omniEntries + minimedEntries + danaEntries + medtrumEntries + libreLoopEntries + trioAlgorithmEntries
+
+    static func lookup(_ identifier: Alert.Identifier) -> Alert.CatalogEntry? {
+        if let exact = entries.first(where: { $0.identifier == identifier }) {
+            return exact
+        }
+        return omniPodFaultEntry(for: identifier)
+    }
+
+    /// Omni emits pod faults via `notifyPodFault` with a separate manager
+    /// identifier (`Omni:pumpFault`) and a formatted alert identifier
+    /// (`Fault Event Code 0xNN: ...`). Map the small set of user-relevant
+    /// codes to concrete concepts; everything else falls back to a generic
+    /// `Pod Fault` so it still surfaces under the Critical tier.
+    private static func omniPodFaultEntry(for identifier: Alert.Identifier) -> Alert.CatalogEntry? {
+        guard identifier.managerIdentifier == "Omni:pumpFault" else { return nil }
+        let code = parseOmniFaultHexCode(from: identifier.alertIdentifier)
+        switch code {
+        case 0x14:
+            return Alert.CatalogEntry(
+                identifier: identifier, interruptionLevel: .critical,
+                title: "Pod Occlusion", category: "Delivery", concept: .occlusion
+            )
+        case 0x18:
+            return Alert.CatalogEntry(
+                identifier: identifier, interruptionLevel: .critical,
+                title: "Pod Reservoir Empty", category: "Reservoir", concept: .reservoirEmpty
+            )
+        case 0x1C:
+            return Alert.CatalogEntry(
+                identifier: identifier, interruptionLevel: .timeSensitive,
+                title: "Pod Expired", category: "Lifecycle", concept: .deviceExpired
+            )
+        default:
+            return Alert.CatalogEntry(
+                identifier: identifier, interruptionLevel: .critical,
+                title: "Pod Fault", category: "Hardware", concept: .hardwareFault
+            )
+        }
+    }
+
+    /// Extracts the hex code from `Fault Event Code 0xNN: ...`. Bounded
+    /// parser, not a substring classifier — the prefix shape is fixed by
+    /// `FaultEventCode.description`.
+    private static func parseOmniFaultHexCode(from alertID: String) -> UInt8? {
+        let prefix = "Fault Event Code 0x"
+        guard alertID.hasPrefix(prefix), alertID.count >= prefix.count + 2 else { return nil }
+        let start = alertID.index(alertID.startIndex, offsetBy: prefix.count)
+        let end = alertID.index(start, offsetBy: 2)
+        return UInt8(alertID[start ..< end], radix: 16)
+    }
+}
+
+// MARK: - Omnipod (Eros + DASH)
+
+private extension AlertCatalogRegistry {
+    static let omniEntries: [Alert.CatalogEntry] = [
+        addEntry("Omni", "userPodExpiration", .active, "Pod Expiration Reminder", "Lifecycle", .deviceExpirationReminder),
+        addEntry("Omni", "podExpiring", .timeSensitive, "Pod Expired", "Lifecycle", .deviceExpired),
+        addEntry("Omni", "podExpireImminent", .timeSensitive, "Pod Shutdown Imminent", "Lifecycle", .deviceShutdownImminent),
+        addEntry("Omni", "lowReservoir", .timeSensitive, "Low Reservoir", "Reservoir", .reservoirLow),
+        addEntry("Omni", "suspendInProgress", .active, "Suspend In Progress Reminder", "Delivery", .suspendInProgressReminder),
+        addEntry("Omni", "suspendEnded", .timeSensitive, "Resume Insulin", "Delivery", .insulinResumeReminder),
+        addEntry("Omni", "finishSetupReminder", .active, "Pod Pairing Incomplete", "Lifecycle", .setupIncomplete),
+        addEntry("Omni", "unexpectedAlert", .critical, "Unexpected Alert", "Hardware", .hardwareFault),
+        addEntry("Omni", "timeOffsetChangeDetected", .active, "Time Change Detected", "Lifecycle", .timeChange),
+        addEntry("Omni", "lowRLBattery", .timeSensitive, "Low RileyLink Battery", "Battery", .rileyLinkBatteryLow)
+    ]
+}
+
+// MARK: - Minimed (500/700)
+
+private extension AlertCatalogRegistry {
+    static let minimedEntries: [Alert.CatalogEntry] = [
+        addEntry("Minimed", "PumpBatteryLow", .timeSensitive, "Pump Battery Low", "Battery", .pumpBatteryLow),
+        addEntry("Minimed", "PumpReservoirEmpty", .critical, "Pump Reservoir Empty", "Reservoir", .reservoirEmpty),
+        addEntry("Minimed", "PumpReservoirLow", .timeSensitive, "Pump Reservoir Low", "Reservoir", .reservoirLow),
+        addEntry("Minimed", "lowRLBattery", .timeSensitive, "Low RileyLink Battery", "Battery", .rileyLinkBatteryLow)
+    ]
+}
+
+// MARK: - Dana (RS/i/-i)
+
+private extension AlertCatalogRegistry {
+    static let danaEntries: [Alert.CatalogEntry] = [
+        addEntry("Dana", "batteryZeroPercent", .critical, "Pump Battery 0%", "Battery", .pumpBatteryEmpty),
+        addEntry("Dana", "pumpError", .critical, "Pump Error", "Hardware", .hardwareFault),
+        addEntry("Dana", "occlusion", .critical, "Occlusion", "Delivery", .occlusion),
+        addEntry("Dana", "lowBattery", .timeSensitive, "Low Pump Battery", "Battery", .pumpBatteryLow),
+        addEntry("Dana", "shutdown", .critical, "Pump Shutdown", "Hardware", .hardwareFault),
+        addEntry("Dana", "basalCompare", .active, "Basal Compare", "Delivery", .basalProfileMismatch),
+        addEntry("Dana", "bloodSugarMeasure", .active, "Blood Glucose Measure", "Reminders", .userBloodGlucoseReminder),
+        addEntry("Dana", "remainingInsulinLevel", .timeSensitive, "Remaining Insulin Level", "Reservoir", .reservoirLow),
+        addEntry("Dana", "emptyReservoir", .critical, "Empty Reservoir", "Reservoir", .reservoirEmpty),
+        addEntry("Dana", "checkShaft", .critical, "Check Shaft", "Hardware", .hardwareFault),
+        addEntry("Dana", "basalMax", .active, "Basal Limit Reached", "Delivery", .insulinLimitWarning),
+        addEntry("Dana", "dailyMax", .active, "Daily Limit Reached", "Delivery", .insulinLimitWarning),
+        addEntry("Dana", "bloodSugarCheckMiss", .active, "Missed Blood Glucose Check", "Reminders", .userBloodGlucoseReminder),
+        addEntry("Dana", "ble5InvalidKeys", .timeSensitive, "Pairing Failed", "Connectivity", .pairingFailed),
+        addEntry("Dana", "unknown", .timeSensitive, "Unknown Pump Error", "Hardware", .hardwareFault)
+    ]
+}
+
+// MARK: - Medtrum (TouchCare nano)
+
+private extension AlertCatalogRegistry {
+    static let medtrumEntries: [Alert.CatalogEntry] = [
+        addEntry(
+            "Medtrum",
+            "com.nightscout.medtrumkit.patch-expired",
+            .active,
+            "Patch Expiring Soon",
+            "Lifecycle",
+            .deviceExpirationReminder
+        ),
+        addEntry(
+            "Medtrum",
+            "com.nightscout.medtrumkit.patch-daily-limit",
+            .timeSensitive,
+            "Daily Insulin Limit",
+            "Delivery",
+            .insulinLimitReached
+        ),
+        addEntry(
+            "Medtrum",
+            "com.nightscout.medtrumkit.patch-hourly-limit",
+            .timeSensitive,
+            "Hourly Insulin Limit",
+            "Delivery",
+            .insulinLimitReached
+        ),
+        // MedtrumKit currently emits the occlusion alert with a misspelled
+        // identifier ("patch-occlussion", double "s") — see MedtrumKit's
+        // NotificationManager. We register both the misspelled identifier (what
+        // ships today) and the corrected spelling so the alert escalates to
+        // critical regardless of which MedtrumKit is bundled. Drop the
+        // misspelled entry once the typo is fixed upstream.
+        addEntry("Medtrum", "com.nightscout.medtrumkit.patch-occlussion", .critical, "Occlusion", "Delivery", .occlusion),
+        addEntry("Medtrum", "com.nightscout.medtrumkit.patch-occlusion", .critical, "Occlusion", "Delivery", .occlusion),
+        addEntry("Medtrum", "com.nightscout.medtrumkit.patch-fault", .critical, "Patch Fault", "Hardware", .hardwareFault),
+        addEntry("Medtrum", "com.nightscout.medtrumkit.patch-empty", .critical, "Reservoir Empty", "Reservoir", .reservoirEmpty),
+        addEntry(
+            "Medtrum",
+            "com.nightscout.medtrumkit.reservoir-low",
+            .timeSensitive,
+            "Reservoir Low",
+            "Reservoir",
+            .reservoirLow
+        )
+    ]
+}
+
+// MARK: - LibreLoop (FreeStyle Libre 3)
+
+private extension AlertCatalogRegistry {
+    /// Manager identifier matches `LibreLoopCGMManager.pluginIdentifier`.
+    /// Alert identifiers come from `LibreLoopExpiryAlerts` +
+    /// `LibreLoopCGMManager.sensorAttentionAlertID` /
+    /// `needsReScanAlertID`. The 24 h warning is advance notice
+    /// (`.timeSensitive`); the 2 h warning, end-of-session, and replace-
+    /// needed escalate to `.critical`. Reconnect-needs-rescan is a user-
+    /// action prompt — `.timeSensitive`.
+    static let libreLoopEntries: [Alert.CatalogEntry] = [
+        addEntry(
+            "LibreLoopCGMManager",
+            "sensorExpiry.warning24h",
+            .timeSensitive,
+            "Sensor Expires in 24 Hours",
+            "Sensor",
+            .cgmExpiringSoon
+        ),
+        addEntry(
+            "LibreLoopCGMManager",
+            "sensorExpiry.warning2h",
+            .critical,
+            "Sensor Expires in 2 Hours",
+            "Sensor",
+            .cgmExpiringSoon
+        ),
+        addEntry(
+            "LibreLoopCGMManager",
+            "sensorExpiry.sessionEnded",
+            .critical,
+            "Sensor Session Ended",
+            "Sensor",
+            .cgmExpired
+        ),
+        addEntry(
+            "LibreLoopCGMManager",
+            "sensorAttention",
+            .critical,
+            "Replace Sensor",
+            "Sensor",
+            .cgmReplacementNeeded
+        ),
+        addEntry(
+            "LibreLoopCGMManager",
+            "reconnectNeedsReScan",
+            .timeSensitive,
+            "Re-scan Sensor",
+            "Connectivity",
+            .cgmReconnectNeeded
+        )
+    ]
+}
+
+// MARK: - Trio internal — Algorithm
+
+private extension AlertCatalogRegistry {
+    /// Trio-internal alerts emitted under `manager = "trio.aps"`. Levels
+    /// match what `APSManager.issueAlertFor…` / `NotLoopingMonitor` set at
+    /// construction; the catalog presence is what lets these surface in the
+    /// Device Alarms screen alongside pump and CGM entries.
+    /// `loop.notActive` matches `NotLoopingMonitor.alertID`; other slugs
+    /// come from `TrioAlertCategory.alertIdentifier`.
+    static let trioAlgorithmEntries: [Alert.CatalogEntry] = [
+        addEntry("trio.aps", "loop.notActive", .critical, "Trio Not Looping", "Algorithm", .notLooping),
+        addEntry("trio.aps", "algorithmError", .active, "Algorithm Error", "Algorithm", .algorithmError),
+        addEntry("trio.aps", "glucoseDataStale", .timeSensitive, "Glucose Data Stale", "Algorithm", .glucoseDataStale)
+    ]
+}
+
+// MARK: - Helpers
+
+private extension AlertCatalogRegistry {
+    static func addEntry(
+        _ manager: String,
+        _ alertID: String,
+        _ level: Alert.InterruptionLevel,
+        _ title: String,
+        _ category: String,
+        _ concept: Alert.CatalogConcept
+    ) -> Alert.CatalogEntry {
+        Alert.CatalogEntry(
+            managerIdentifier: manager,
+            alertIdentifier: alertID,
+            interruptionLevel: level,
+            title: title,
+            category: category,
+            concept: concept
+        )
+    }
+}

+ 129 - 0
Trio/Sources/Services/Alerts/AlertCatalogVendor.swift

@@ -0,0 +1,129 @@
+import Foundation
+import LoopKit
+
+/// Plugin-side declaration of every alert a device manager may issue.
+/// Mirrors LoopKit's `AlertSoundVendor` pattern (static, boot-time, no
+/// side effects). Trio uses the union catalog to drive per-alert severity,
+/// the Settings UI, and per-alert mute. When upstream LoopKit adopts this,
+/// Trio's bridge extensions move into the plugin repos verbatim.
+protocol AlertCatalogVendor {
+    var alertCatalog: [Alert.CatalogEntry] { get }
+}
+
+extension Alert {
+    /// Cross-plugin canonical key for the Settings UI. Multiple per-plugin
+    /// entries with the same concept collapse to a single row.
+    enum CatalogConcept: Hashable {
+        case occlusion
+        case reservoirLow
+        case reservoirEmpty
+        case pumpBatteryLow
+        case pumpBatteryEmpty
+        case rileyLinkBatteryLow
+        case hardwareFault
+        case insulinLimitWarning
+        case insulinLimitReached
+        case suspendInProgressReminder
+        case insulinResumeReminder
+        case deviceExpirationReminder
+        case deviceExpired
+        case deviceShutdownImminent
+        case setupIncomplete
+        case timeChange
+        case pairingFailed
+        case userBloodGlucoseReminder
+        case basalProfileMismatch
+        // CGM-side concepts. Surface through `TrioAlertManager` via
+        // `PluginSource.issueAlert(_:)`.
+        case cgmExpiringSoon
+        case cgmExpired
+        case cgmReplacementNeeded
+        case cgmReconnectNeeded
+        // Trio-internal "Algorithm" concepts. Emitted by `APSManager` /
+        // `NotLoopingMonitor` under the `trio.aps` manager identifier.
+        case notLooping
+        case algorithmError
+        case glucoseDataStale
+        /// Plugin-unique alarm that doesn't merge with anything else.
+        case unspecified
+
+        var displayTitle: String {
+            switch self {
+            case .occlusion: return String(localized: "Occlusion")
+            case .reservoirLow: return String(localized: "Reservoir Low")
+            case .reservoirEmpty: return String(localized: "Reservoir Empty")
+            case .pumpBatteryLow: return String(localized: "Pump Battery Low")
+            case .pumpBatteryEmpty: return String(localized: "Pump Battery Empty")
+            case .rileyLinkBatteryLow: return String(localized: "RileyLink Battery Low")
+            case .hardwareFault: return String(localized: "Pump Fault")
+            case .insulinLimitWarning: return String(localized: "Insulin Limit Warning")
+            case .insulinLimitReached: return String(localized: "Insulin Limit Reached")
+            case .suspendInProgressReminder: return String(localized: "Suspend In Progress")
+            case .insulinResumeReminder: return String(localized: "Insulin Resume Reminder")
+            case .deviceExpirationReminder: return String(localized: "Device Expiration Reminder")
+            case .deviceExpired: return String(localized: "Device Expired")
+            case .deviceShutdownImminent: return String(localized: "Device Shutdown Imminent")
+            case .setupIncomplete: return String(localized: "Setup Incomplete")
+            case .timeChange: return String(localized: "Time Change Detected")
+            case .pairingFailed: return String(localized: "Pairing Failed")
+            case .userBloodGlucoseReminder: return String(localized: "Blood Glucose Reminder")
+            case .basalProfileMismatch: return String(localized: "Basal Profile Mismatch")
+            case .cgmExpiringSoon: return String(localized: "CGM Sensor Expires Soon")
+            case .cgmExpired: return String(localized: "CGM Sensor Expired")
+            case .cgmReplacementNeeded: return String(localized: "Replace CGM Sensor")
+            case .cgmReconnectNeeded: return String(localized: "CGM Reconnect Required")
+            case .notLooping: return String(localized: "Trio Not Looping")
+            case .algorithmError: return String(localized: "Algorithm Error")
+            case .glucoseDataStale: return String(localized: "Glucose Data Stale")
+            case .unspecified: return ""
+            }
+        }
+    }
+
+    struct CatalogEntry: Identifiable, Equatable {
+        let identifier: Alert.Identifier
+        let interruptionLevel: Alert.InterruptionLevel
+        /// Plugin's authentic title — surfaces in logs / debug paths.
+        let title: String
+        /// Soft grouping ("Delivery", "Reservoir", "Hardware", ...).
+        let category: String
+        /// UI dedupe key — entries sharing a concept collapse to one row.
+        let concept: CatalogConcept
+
+        var id: String { identifier.value }
+
+        init(
+            identifier: Alert.Identifier,
+            interruptionLevel: Alert.InterruptionLevel,
+            title: String,
+            category: String,
+            concept: CatalogConcept
+        ) {
+            self.identifier = identifier
+            self.interruptionLevel = interruptionLevel
+            self.title = title
+            self.category = category
+            self.concept = concept
+        }
+
+        init(
+            managerIdentifier: String,
+            alertIdentifier: Alert.AlertIdentifier,
+            interruptionLevel: Alert.InterruptionLevel,
+            title: String,
+            category: String,
+            concept: CatalogConcept
+        ) {
+            self.init(
+                identifier: Alert.Identifier(
+                    managerIdentifier: managerIdentifier,
+                    alertIdentifier: alertIdentifier
+                ),
+                interruptionLevel: interruptionLevel,
+                title: title,
+                category: category,
+                concept: concept
+            )
+        }
+    }
+}

+ 0 - 0
Trio/Sources/Services/Alerts/CGMManagerAlertOwnership.swift


Some files were not shown because too many files changed in this diff