Parcourir la source

Merge branch 'dev' into feat/dev-eversense

Deniz Cengiz il y a 1 semaine
Parent
commit
1e780a4519

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit 98fae7929c8c8e4e849d18a70c1f249dd6c09e5f
+Subproject commit 26ad72115d3b98b05668d57b817b1bf961f8a721

+ 1 - 1
Config.xcconfig

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

+ 1 - 1
MedtrumKit

@@ -1 +1 @@
-Subproject commit 7a3cb276b65ebaee7a3d6485878baa525399c3ee
+Subproject commit 1c2d10fd73478f37773f96facf30ff63dbbc05e1

+ 20 - 0
Model/CoreDataStack.swift

@@ -113,11 +113,31 @@ class CoreDataStack: ObservableObject {
     func fetchPersistentHistory() async {
         do {
             try await fetchPersistentHistoryTransactionsAndChanges()
+        } catch let error as NSError where error.code == NSPersistentHistoryTokenExpiredError {
+            debug(.coreData, "Persistent history token expired; clearing token and replaying available history.")
+            await recoverFromExpiredHistoryToken()
         } catch {
             debug(.coreData, "\(error)")
         }
     }
 
+    /// Recovers the change-merge pipeline after the persistent history token expires.
+    private func recoverFromExpiredHistoryToken() async {
+        lastToken = nil
+        do {
+            try await fetchPersistentHistoryTransactionsAndChanges()
+        } catch {
+            debug(.coreData, "Replay after token reset failed; jumping to current token. \(error)")
+            let viewContext = persistentContainer.viewContext
+            let currentToken = persistentContainer.persistentStoreCoordinator
+                .currentPersistentHistoryToken(fromStores: nil)
+            lastToken = currentToken
+            await viewContext.perform {
+                viewContext.refreshAllObjects()
+            }
+        }
+    }
+
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
         let taskContext = newTaskContext()
         taskContext.name = "persistentHistoryContext"

+ 125 - 101
Trio.xcodeproj/project.pbxproj

@@ -138,6 +138,9 @@
 		38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */; };
 		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
 		38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */; };
+		BD11C001000000000000C001 /* CGMSensorDisplayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11C000000000000000C001 /* CGMSensorDisplayState.swift */; };
+		BD11C001000000000000C003 /* SensorLifecycleArcView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11C000000000000000C003 /* SensorLifecycleArcView.swift */; };
+		BD11C001000000000000C004 /* SensorStatusTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11C000000000000000C004 /* SensorStatusTagView.swift */; };
 		38AEE73D25F0200C0013F05B /* TrioSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE73C25F0200C0013F05B /* TrioSettings.swift */; };
 		38AEE75225F022080013F05B /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75125F022080013F05B /* SettingsManager.swift */; };
 		38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75625F0F18E0013F05B /* CarbsStorage.swift */; };
@@ -190,7 +193,6 @@
 		38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A3625F5509500C0CED0 /* String+Extensions.swift */; };
 		38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */; };
 		38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */; };
-		DDB0BBA02026050100000001 /* BolusProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0BBA02026050100000002 /* BolusProgressBar.swift */; };
 		38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F37827261260DC009DB701 /* Color+Extensions.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
@@ -314,13 +316,14 @@
 		3BEF6AB32D97316F0076089D /* MealTotalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEF6AB22D97316A0076089D /* MealTotalTests.swift */; };
 		3BF424C72DF4805A0017CFD9 /* AutosensError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF424C62DF480550017CFD9 /* AutosensError.swift */; };
 		3BF92F3A2D86F1AA006B545A /* iob-error-log.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BF92F392D86F1AA006B545A /* iob-error-log.json */; };
-		B015AFE32E500000000D7351 /* BolusSafetyValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */; };
 		3E28F2AB2EB5337F00FB9EEB /* ConnectIQ in Frameworks */ = {isa = PBXBuildFile; productRef = 3E28F2AA2EB5337F00FB9EEB /* ConnectIQ */; };
 		3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; };
 		3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3E62C7822F54CC1B00433237 /* BolusDisplayThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */; };
 		3E84DA402F48D96000033608 /* EversenseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E84DA3F2F48D96000033608 /* EversenseKit.framework */; };
 		3E84DA412F48D96000033608 /* EversenseKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E84DA3F2F48D96000033608 /* EversenseKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3EF667132FE48509009FB31A /* BasalDeliveryState+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF667122FE48502009FB31A /* BasalDeliveryState+Extension.swift */; };
+		41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		49090A8D2E9FE8D200D0F5DB /* GarminWatchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */; };
@@ -403,18 +406,34 @@
 		9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */; };
 		9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A48AE3AC813A49A517846A /* NightscoutConfigStateModel.swift */; };
 		98641AF4F92123DA668AB931 /* CarbRatioEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */; };
-		41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */; };
 		A33352ED40476125EBAC6EE0 /* CarbRatioEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */; };
+		AC19EF2C94084B5BA0175D1D /* SettingsSearchHighlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */; };
 		AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */; };
+		B015AFE32E500000000D7351 /* BolusSafetyValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */; };
+		B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */; };
 		B6E925132EB3932A0076D719 /* OmnipodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6E925122EB3932A0076D719 /* OmnipodKit.framework */; };
 		B6E925142EB3932A0076D719 /* OmnipodKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B6E925122EB3932A0076D719 /* OmnipodKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		B7C465E9472624D8A2BE2A6A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
+		BB5227A51D9D4377A1A70BA6 /* TempTargetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD10516D2DA986E1007C6D89 /* PulsingLogoAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
+		BD175EBE0000100000000001 /* HistoryDataFlow+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */; };
+		BD175EBE0000100000000002 /* HistoryDeletionTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */; };
+		BD175EBE0000100000000003 /* HistoryStateModel+Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */; };
+		BD175EBE0000100000000004 /* HistoryStateModel+Carbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */; };
+		BD175EBE0000100000000005 /* HistoryStateModel+Insulin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */; };
+		BD175EBE0000100000000006 /* HistoryStateModel+CarbEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */; };
+		BD175EBE0000100000000007 /* HistoryRootView+Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */; };
+		BD175EBE0000100000000008 /* HistoryRootView+Meals.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */; };
+		BD175EBE0000100000000009 /* HistoryRootView+Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */; };
+		BD175EBE000010000000000A /* HistoryRootView+Adjustments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */; };
+		BD175EBE000010000000000B /* HistoryRootView+Filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000B /* HistoryRootView+Filters.swift */; };
+		BD175EBE000010000000000C /* HistoryRootView+AddGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */; };
+		BD175EBE000010000000000D /* HistoryRootView+Confirmations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */; };
 		BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */; };
 		BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D872D42FBFB00412DEB /* BolusStatsView.swift */; };
 		BD249D8A2D42FC1200412DEB /* GlucosePercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */; };
@@ -474,7 +493,6 @@
 		BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0582D66189700B95AED /* TestAssembly.swift */; };
 		BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */; };
 		BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */; };
-		B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */; };
 		BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */; };
 		BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */; };
 		BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */; };
@@ -493,7 +511,6 @@
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
-		BB5227A51D9D4377A1A70BA6 /* TempTargetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
@@ -610,7 +627,6 @@
 		DD1745242C55526000211FAC /* SMBSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745232C55526000211FAC /* SMBSettingsStateModel.swift */; };
 		DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745252C55526F00211FAC /* SMBSettingsRootView.swift */; };
 		DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745282C55642100211FAC /* SettingInputSection.swift */; };
-		AC19EF2C94084B5BA0175D1D /* SettingsSearchHighlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */; };
 		DD17452B2C556E8100211FAC /* SettingInputHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452A2C556E8100211FAC /* SettingInputHintView.swift */; };
 		DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452D2C55AE4800211FAC /* TargetBehavoirDataFlow.swift */; };
 		DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452F2C55AE5300211FAC /* TargetBehaviorProvider.swift */; };
@@ -695,16 +711,16 @@
 		DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */; };
 		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD758EDE2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD758EDD2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift */; };
-		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
-		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD7E1E300000000000000002 /* TelemetryClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000001 /* TelemetryClient.swift */; };
-		DD7E1E300000000000000014 /* TelemetryAttestor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000013 /* TelemetryAttestor.swift */; };
 		DD7E1E300000000000000004 /* TelemetryPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000003 /* TelemetryPreviewView.swift */; };
 		DD7E1E300000000000000006 /* TelemetryPrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000005 /* TelemetryPrivacyView.swift */; };
 		DD7E1E300000000000000008 /* TelemetryMigrationSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000007 /* TelemetryMigrationSheetView.swift */; };
 		DD7E1E30000000000000000E /* TelemetryDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E30000000000000000D /* TelemetryDataFlow.swift */; };
 		DD7E1E300000000000000010 /* TelemetryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E30000000000000000F /* TelemetryProvider.swift */; };
 		DD7E1E300000000000000012 /* TelemetryStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000011 /* TelemetryStateModel.swift */; };
+		DD7E1E300000000000000014 /* TelemetryAttestor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E1E300000000000000013 /* TelemetryAttestor.swift */; };
+		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
+		DD82D4B82DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */; };
 		DD868FD82E381A54005D3308 /* APNSJWTClaims.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD906BF42EA6AA0100262772 /* NightscoutUploadPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD906BF32EA6AA0100262772 /* NightscoutUploadPipeline.swift */; };
@@ -730,6 +746,7 @@
 		DDA9AC092D672CF100E6F1A9 /* AppVersionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9AC082D672CEB00E6F1A9 /* AppVersionChecker.swift */; };
 		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
+		DDB0BBA02026050100000001 /* BolusProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0BBA02026050100000002 /* BolusProgressBar.swift */; };
 		DDB0E3742DB1BAC1004B826F /* LogoBurstSplash.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
@@ -834,19 +851,6 @@
 		FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */; };
 		FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */; };
 		FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */; };
-		BD175EBE0000100000000001 /* HistoryDataFlow+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */; };
-		BD175EBE0000100000000002 /* HistoryDeletionTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */; };
-		BD175EBE0000100000000003 /* HistoryStateModel+Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */; };
-		BD175EBE0000100000000004 /* HistoryStateModel+Carbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */; };
-		BD175EBE0000100000000005 /* HistoryStateModel+Insulin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */; };
-		BD175EBE0000100000000006 /* HistoryStateModel+CarbEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */; };
-		BD175EBE0000100000000007 /* HistoryRootView+Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */; };
-		BD175EBE0000100000000008 /* HistoryRootView+Meals.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */; };
-		BD175EBE0000100000000009 /* HistoryRootView+Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */; };
-		BD175EBE000010000000000A /* HistoryRootView+Adjustments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */; };
-		BD175EBE000010000000000B /* HistoryRootView+Filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000B /* HistoryRootView+Filters.swift */; };
-		BD175EBE000010000000000C /* HistoryRootView+AddGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */; };
-		BD175EBE000010000000000D /* HistoryRootView+Confirmations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -1101,6 +1105,9 @@
 		38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = "<group>"; };
 		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
 		38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentGlucoseView.swift; sourceTree = "<group>"; };
+		BD11C000000000000000C001 /* CGMSensorDisplayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMSensorDisplayState.swift; sourceTree = "<group>"; };
+		BD11C000000000000000C003 /* SensorLifecycleArcView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorLifecycleArcView.swift; sourceTree = "<group>"; };
+		BD11C000000000000000C004 /* SensorStatusTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorStatusTagView.swift; sourceTree = "<group>"; };
 		38AEE73C25F0200C0013F05B /* TrioSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioSettings.swift; sourceTree = "<group>"; };
 		38AEE75125F022080013F05B /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
 		38AEE75625F0F18E0013F05B /* CarbsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorage.swift; sourceTree = "<group>"; };
@@ -1151,7 +1158,6 @@
 		38E98A3625F5509500C0CED0 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
 		38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleLogReporter.swift; sourceTree = "<group>"; };
 		38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressViewStyle.swift; sourceTree = "<group>"; };
-		DDB0BBA02026050100000002 /* BolusProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressBar.swift; sourceTree = "<group>"; };
 		38F37827261260DC009DB701 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
 		38F3783A2613555C009DB701 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
 		38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsStorage.swift; sourceTree = "<group>"; };
@@ -1259,14 +1265,15 @@
 		3BF424C62DF480550017CFD9 /* AutosensError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutosensError.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3BF92F392D86F1AA006B545A /* iob-error-log.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "iob-error-log.json"; sourceTree = "<group>"; };
-		B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidator.swift; sourceTree = "<group>"; };
 		3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MedtrumKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusDisplayThreshold.swift; sourceTree = "<group>"; };
 		3E84DA3F2F48D96000033608 /* EversenseKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EversenseKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3EF667122FE48502009FB31A /* BasalDeliveryState+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BasalDeliveryState+Extension.swift"; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
 		3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
 		42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
 		44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorProvider.swift; sourceTree = "<group>"; };
+		48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchHighlight.swift; sourceTree = "<group>"; };
 		49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchSettings.swift; sourceTree = "<group>"; };
 		491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
@@ -1276,6 +1283,7 @@
 		4984D1D32EA2939E00263E83 /* WatchConfigGarminAppConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigGarminAppConfigView.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		49C782A62F73D9870062B0DD /* AlertEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertEntry.swift; sourceTree = "<group>"; };
+		4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetData.swift; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingIdentifiers.swift; sourceTree = "<group>"; };
@@ -1349,6 +1357,9 @@
 		A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigProvider.swift; sourceTree = "<group>"; };
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
+		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>"; };
@@ -1357,6 +1368,19 @@
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
 		BD10516C2DA986DC007C6D89 /* PulsingLogoAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulsingLogoAnimation.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
+		BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryDataFlow+Models.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryDeletionTarget.swift; sourceTree = "<group>"; };
+		BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Glucose.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Carbs.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Insulin.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+CarbEditing.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Treatments.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Meals.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Glucose.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Adjustments.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000B /* HistoryRootView+Filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Filters.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+AddGlucose.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Confirmations.swift"; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
 		BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMetricsView.swift; sourceTree = "<group>"; };
 		BD249D872D42FBFB00412DEB /* BolusStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusStatsView.swift; sourceTree = "<group>"; };
@@ -1411,7 +1435,6 @@
 		BD8FC0582D66189700B95AED /* TestAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssembly.swift; sourceTree = "<group>"; };
 		BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorTests.swift; sourceTree = "<group>"; };
-		B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidatorTests.swift; sourceTree = "<group>"; };
 		BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStorageTests.swift; sourceTree = "<group>"; };
@@ -1430,7 +1453,6 @@
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
-		4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetData.swift; sourceTree = "<group>"; };
 		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
@@ -1554,7 +1576,6 @@
 		DD1745232C55526000211FAC /* SMBSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745252C55526F00211FAC /* SMBSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1745282C55642100211FAC /* SettingInputSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInputSection.swift; sourceTree = "<group>"; };
-		48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchHighlight.swift; sourceTree = "<group>"; };
 		DD17452A2C556E8100211FAC /* SettingInputHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInputHintView.swift; sourceTree = "<group>"; };
 		DD17452D2C55AE4800211FAC /* TargetBehavoirDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehavoirDataFlow.swift; sourceTree = "<group>"; };
 		DD17452F2C55AE5300211FAC /* TargetBehaviorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehaviorProvider.swift; sourceTree = "<group>"; };
@@ -1635,16 +1656,16 @@
 		DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentMenuView.swift; sourceTree = "<group>"; };
 		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD758EDD2ECC656500EF5D54 /* DetermineBasalSmbMicroBolusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalSmbMicroBolusTests.swift; sourceTree = "<group>"; };
-		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
-		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD7E1E300000000000000001 /* TelemetryClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryClient.swift; sourceTree = "<group>"; };
-		DD7E1E300000000000000013 /* TelemetryAttestor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryAttestor.swift; sourceTree = "<group>"; };
 		DD7E1E300000000000000003 /* TelemetryPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryPreviewView.swift; sourceTree = "<group>"; };
 		DD7E1E300000000000000005 /* TelemetryPrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryPrivacyView.swift; sourceTree = "<group>"; };
 		DD7E1E300000000000000007 /* TelemetryMigrationSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryMigrationSheetView.swift; sourceTree = "<group>"; };
 		DD7E1E30000000000000000D /* TelemetryDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryDataFlow.swift; sourceTree = "<group>"; };
 		DD7E1E30000000000000000F /* TelemetryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryProvider.swift; sourceTree = "<group>"; };
 		DD7E1E300000000000000011 /* TelemetryStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryStateModel.swift; sourceTree = "<group>"; };
+		DD7E1E300000000000000013 /* TelemetryAttestor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryAttestor.swift; sourceTree = "<group>"; };
+		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
+		DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyPersistentFlags.swift; sourceTree = "<group>"; };
 		DD868FD72E381A54005D3308 /* APNSJWTClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSJWTClaims.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD906BF32EA6AA0100262772 /* NightscoutUploadPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUploadPipeline.swift; sourceTree = "<group>"; };
@@ -1671,6 +1692,7 @@
 		DDA9AC0A2D678DAD00E6F1A9 /* blacklisted-versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blacklisted-versions.json"; sourceTree = "<group>"; };
 		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
 		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
+		DDB0BBA02026050100000002 /* BolusProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressBar.swift; sourceTree = "<group>"; };
 		DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoBurstSplash.swift; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
@@ -1762,7 +1784,6 @@
 		E592A3742CEEC038009A472C /* ContactImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageProvider.swift; sourceTree = "<group>"; };
 		E592A3752CEEC038009A472C /* ContactImageStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStateModel.swift; sourceTree = "<group>"; };
 		E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsProvider.swift; sourceTree = "<group>"; };
-		B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsSearchTests.swift; sourceTree = "<group>"; };
 		F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = "<group>"; };
 		F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = "<group>"; };
 		F90692A9274B7AAE0037068D /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
@@ -1774,19 +1795,6 @@
 		FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutPreferences.swift; sourceTree = "<group>"; };
 		FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
 		FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryDataFlow+Models.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryDeletionTarget.swift; sourceTree = "<group>"; };
-		BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Glucose.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Carbs.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Insulin.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+CarbEditing.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Treatments.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Meals.swift"; sourceTree = "<group>"; };
-		BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Glucose.swift"; sourceTree = "<group>"; };
-		BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Adjustments.swift"; sourceTree = "<group>"; };
-		BD175EFE000010000000000B /* HistoryRootView+Filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Filters.swift"; sourceTree = "<group>"; };
-		BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+AddGlucose.swift"; sourceTree = "<group>"; };
-		BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Confirmations.swift"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -2299,36 +2307,6 @@
 			path = Services;
 			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 = (
@@ -2521,11 +2499,22 @@
 				DDA6E2842D2361F800C2988C /* LoopStatusView.swift */,
 				383420D525FFE38C002D46C1 /* LoopView.swift */,
 				38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */,
+				BD11C002000000000000C000 /* SensorLifecycle */,
 				38DAB27F260CBB7F00F74C1A /* PumpView.swift */,
 			);
 			path = Header;
 			sourceTree = "<group>";
 		};
+		BD11C002000000000000C000 /* SensorLifecycle */ = {
+			isa = PBXGroup;
+			children = (
+				BD11C000000000000000C001 /* CGMSensorDisplayState.swift */,
+				BD11C000000000000000C003 /* SensorLifecycleArcView.swift */,
+				BD11C000000000000000C004 /* SensorStatusTagView.swift */,
+			);
+			path = SensorLifecycle;
+			sourceTree = "<group>";
+		};
 		3856933F270B57A00002C50D /* CGM */ = {
 			isa = PBXGroup;
 			children = (
@@ -2696,6 +2685,7 @@
 			children = (
 				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
 				DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */,
+				3EF667122FE48502009FB31A /* BasalDeliveryState+Extension.swift */,
 				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
 				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
 				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
@@ -3057,14 +3047,6 @@
 			path = JSONImporterData;
 			sourceTree = "<group>";
 		};
-		B015AFE22E500000000D7351 /* BolusSafety */ = {
-			isa = PBXGroup;
-			children = (
-				B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */,
-			);
-			path = BolusSafety;
-			sourceTree = "<group>";
-		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -3238,17 +3220,6 @@
 			path = History;
 			sourceTree = "<group>";
 		};
-		BD175EC00000100000000001 /* HistoryStateModel+Deletion */ = {
-			isa = PBXGroup;
-			children = (
-				BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */,
-				BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */,
-				BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */,
-				BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */,
-			);
-			path = "HistoryStateModel+Deletion";
-			sourceTree = "<group>";
-		};
 		A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */ = {
 			isa = PBXGroup;
 			children = (
@@ -3260,6 +3231,22 @@
 			path = BasalProfileEditor;
 			sourceTree = "<group>";
 		};
+		B015AFE22E500000000D7351 /* BolusSafety */ = {
+			isa = PBXGroup;
+			children = (
+				B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */,
+			);
+			path = BolusSafety;
+			sourceTree = "<group>";
+		};
+		B015AFE62E500000000D7351 /* BolusSafetyTests */ = {
+			isa = PBXGroup;
+			children = (
+				B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */,
+			);
+			path = BolusSafetyTests;
+			sourceTree = "<group>";
+		};
 		B9488883C59C31550E0B4CEC /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -3271,6 +3258,17 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BD175EC00000100000000001 /* HistoryStateModel+Deletion */ = {
+			isa = PBXGroup;
+			children = (
+				BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */,
+				BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */,
+				BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */,
+				BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */,
+			);
+			path = "HistoryStateModel+Deletion";
+			sourceTree = "<group>";
+		};
 		BD249D842D42FBD200412DEB /* ViewElements */ = {
 			isa = PBXGroup;
 			children = (
@@ -3419,14 +3417,6 @@
 			path = BolusCalculatorTests;
 			sourceTree = "<group>";
 		};
-		B015AFE62E500000000D7351 /* BolusSafetyTests */ = {
-			isa = PBXGroup;
-			children = (
-				B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */,
-			);
-			path = BolusSafetyTests;
-			sourceTree = "<group>";
-		};
 		BDA25F1A2D26BCE800035F34 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -3919,6 +3909,36 @@
 			path = StartupGuide;
 			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>";
+		};
 		DD9E6DA02D59A11200514CEC /* Meal */ = {
 			isa = PBXGroup;
 			children = (
@@ -5095,6 +5115,7 @@
 				BD249D9D2D42FCF500412DEB /* MealStatsSetup.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
+				3EF667132FE48509009FB31A /* BasalDeliveryState+Extension.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
@@ -5156,6 +5177,9 @@
 				BDF34F932C10D0E100D51995 /* LiveActivityAttributes+Helper.swift in Sources */,
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
+				BD11C001000000000000C001 /* CGMSensorDisplayState.swift in Sources */,
+				BD11C001000000000000C003 /* SensorLifecycleArcView.swift in Sources */,
+				BD11C001000000000000C004 /* SensorStatusTagView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */,

+ 17 - 26
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "1e72c1cdf8ea5ec9fe527ebfab01ea55fca9e8651fe3252338fd3d4ea2cb327a",
+  "originHash" : "af604046297daf839c0c212e73929960a33a0fecfdb97b64b9356d8a98b7bd60",
   "pins" : [
     {
       "identity" : "abseil-cpp-binary",
@@ -69,17 +69,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/firebase/firebase-ios-sdk.git",
       "state" : {
-        "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5",
-        "version" : "11.15.0"
-      }
-    },
-    {
-      "identity" : "google-ads-on-device-conversion-ios-sdk",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
-      "state" : {
-        "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed",
-        "version" : "2.3.0"
+        "revision" : "d1f7c7e8eaa74d7e44467184dc5f592268247d33",
+        "version" : "11.11.0"
       }
     },
     {
@@ -87,8 +78,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/google/GoogleAppMeasurement.git",
       "state" : {
-        "revision" : "45ce435e9406d3c674dd249a042b932bee006f60",
-        "version" : "11.15.0"
+        "revision" : "dd89fc79a77183830742a16866d87e4e54785734",
+        "version" : "11.11.0"
       }
     },
     {
@@ -105,8 +96,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/google/GoogleUtilities.git",
       "state" : {
-        "revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
-        "version" : "8.1.0"
+        "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
+        "version" : "8.0.2"
       }
     },
     {
@@ -114,8 +105,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/google/grpc-binary.git",
       "state" : {
-        "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
-        "version" : "1.69.1"
+        "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
+        "version" : "1.69.0"
       }
     },
     {
@@ -123,8 +114,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/google/gtm-session-fetcher.git",
       "state" : {
-        "revision" : "c756a29784521063b6a1202907e2cc47f41b667c",
-        "version" : "4.5.0"
+        "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
+        "version" : "4.4.0"
       }
     },
     {
@@ -222,8 +213,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-log.git",
       "state" : {
-        "revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523",
-        "version" : "1.10.1"
+        "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
+        "version" : "1.6.4"
       }
     },
     {
@@ -240,8 +231,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-protobuf.git",
       "state" : {
-        "revision" : "a008af1a102ff3dd6cc3764bb69bf63226d0f5f6",
-        "version" : "1.36.1"
+        "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
+        "version" : "1.29.0"
       }
     },
     {
@@ -276,8 +267,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/Swinject/Swinject",
       "state" : {
-        "revision" : "b685b549fe4d8ae265fc7a2f27d0789720425d69",
-        "version" : "2.10.0"
+        "revision" : "be9dbcc7b86811bc131539a20c6f9c2d3e56919f",
+        "version" : "2.9.1"
       }
     },
     {

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

@@ -70,6 +70,28 @@ enum APSError: LocalizedError {
     }
 }
 
+// MARK: - Thread-safe loop serialization
+
+/// Ensures only one loop runs at a time via actor isolation
+private actor LoopGuard {
+    private var isRunning = false
+
+    /// Atomically checks whether a new loop can start and marks it as running if so.
+    func tryStart(minInterval: TimeInterval, lastLoopDate: Date, lastLoopStartDate: Date) -> Bool {
+        // If the last loop completed after it started, enforce minimum interval
+        if lastLoopDate > lastLoopStartDate {
+            guard lastLoopStartDate.addingTimeInterval(minInterval) < Date() else { return false }
+        }
+        guard !isRunning else { return false }
+        isRunning = true
+        return true
+    }
+
+    func finish() {
+        isRunning = false
+    }
+}
+
 final class BaseAPSManager: APSManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseAPSManager.processQueue")
     @Injected() private var storage: FileStorage!
@@ -89,14 +111,18 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     let privateContext = CoreDataStack.shared.newTaskContext()
 
     private var openAPS: OpenAPS!
 
     private var lifetime = Lifetime()
 
-    private var backgroundTaskID: UIBackgroundTaskIdentifier?
+    private let loopGuard = LoopGuard()
+    /// All reads/writes are dispatched onto `processQueue` so the bolus
+    /// trigger sink, `cancelBolus`, and the `DoseProgressReporter`
+    /// callback (which the pump manager already invokes on
+    /// `processQueue`) all serialize through one queue
+    private var bolusReporter: DoseProgressReporter?
 
     var pumpManager: PumpManagerUI? {
         get { deviceDataManager.pumpManager }
@@ -178,46 +204,48 @@ final class BaseAPSManager: APSManager, Injectable {
         deviceDataManager.errorSubject
             .receive(on: processQueue)
             .map { APSError.pumpError($0) }
-            .sink {
-                self.processError($0)
+            .sink { [weak self] in
+                self?.processError($0)
             }
             .store(in: &lifetime)
 
         deviceDataManager.bolusTrigger
             .receive(on: processQueue)
-            .sink { bolusing in
+            .sink { [weak self] bolusing in
                 if bolusing {
-                    self.createBolusReporter()
+                    self?.createBolusReporter()
                 } else {
-                    self.clearBolusReporter()
+                    self?.clearBolusReporter()
                 }
             }
             .store(in: &lifetime)
 
+        // The following three publishers update `@Persisted` properties that
+        // are also read from the main thread (UI bindings)
         deviceDataManager.scheduledBasal
-            .receive(on: processQueue)
-            .sink { scheduledBasal in
-                self.isScheduledBasal = scheduledBasal
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] scheduledBasal in
+                self?.isScheduledBasal = scheduledBasal
             }
             .store(in: &lifetime)
 
         deviceDataManager.suspended
-            .receive(on: processQueue)
-            .sink { suspended in
-                self.isSuspended = suspended
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] suspended in
+                self?.isSuspended = suspended
             }
             .store(in: &lifetime)
 
         // manage a manual Temp Basal from PumpManager - force loop() after manual temp basal is cancelled or finishes
         deviceDataManager.manualTempBasal
-            .receive(on: processQueue)
-            .sink { manualBasal in
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] manualBasal in
                 if manualBasal {
-                    self.isManualTempBasal = true
+                    self?.isManualTempBasal = true
                 } else {
-                    if self.isManualTempBasal {
-                        self.isManualTempBasal = false
-                        self.loop()
+                    if self?.isManualTempBasal == true {
+                        self?.isManualTempBasal = false
+                        self?.loop()
                     }
                 }
             }
@@ -233,116 +261,92 @@ final class BaseAPSManager: APSManager, Injectable {
         Task { [weak self] in
             guard let self else { return }
 
-            // Check if we can start a new loop
-            guard await self.canStartNewLoop() else { return }
+            // Atomic check-and-set via actor — eliminates the race between
+            // checking isLooping.value and sending isLooping(true).
+            guard await loopGuard.tryStart(
+                minInterval: Config.loopInterval,
+                lastLoopDate: lastLoopDate,
+                lastLoopStartDate: lastLoopStartDate
+            ) else {
+                debug(.apsManager, "Loop skipped (already running or too soon)")
+                return
+            }
+
+            // 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
+            taskID = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
+                // closure runs on the Main Thread
+                // removed the Task that provided no guarantee to end the background task
+                if taskID != .invalid {
+                    UIApplication.shared.endBackgroundTask(taskID)
+                    taskID = .invalid
+                }
+            }
 
-            // Setup loop and background task
-            var (loopStatRecord, backgroundTask) = await self.setupLoop()
+            isLooping.send(true)
 
-            do {
-                // Execute loop logic
-                try await self.executeLoop(loopStatRecord: &loopStatRecord)
+            let loopStartDate = Date()
+            lastLoopStartDate = loopStartDate
+            let interval = await calculateLoopInterval(loopStartDate: loopStartDate)
+
+            var loopStatRecord = LoopStats(
+                start: loopStartDate,
+                loopStatus: "Starting",
+                interval: interval
+            )
 
+            do {
+                try await executeLoop(loopStatRecord: &loopStatRecord)
                 requestNightscoutUpload(
                     [.carbs, .pumpHistory, .overrides, .tempTargets],
                     source: "APSManager"
                 )
+                await finalizeLoop(loopStatRecord: loopStatRecord)
             } catch {
-                var updatedStats = loopStatRecord
-                updatedStats.end = Date()
-                updatedStats.duration = roundDouble((updatedStats.end! - updatedStats.start).timeInterval / 60, 2)
-                updatedStats.loopStatus = error.localizedDescription
-                await loopCompleted(error: error, loopStatRecord: updatedStats)
+                let endDate = Date()
+                loopStatRecord.end = endDate
+                loopStatRecord.duration = roundDouble((endDate - loopStatRecord.start).timeInterval / 60, 2)
+                loopStatRecord.loopStatus = error.localizedDescription
+                await finalizeLoop(error: error, loopStatRecord: loopStatRecord)
                 debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to complete Loop: \(error)")
             }
 
-            // Cleanup background task
-            if let backgroundTask = backgroundTask {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
-                self.backgroundTaskID = .invalid
-            }
-        }
-    }
-
-    private func canStartNewLoop() async -> Bool {
-        // Check if too soon for next loop
-        if lastLoopDate > lastLoopStartDate {
-            guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
-                debug(.apsManager, "Not enough time have passed since last loop at : \(lastLoopStartDate)")
-                return false
-            }
-        }
-
-        // Check if loop already in progress
-        guard !isLooping.value else {
-            warning(.apsManager, "Loop already in progress. Skip recommendation.")
-            return false
-        }
-
-        return true
-    }
-
-    private func setupLoop() async -> (LoopStats, UIBackgroundTaskIdentifier?) {
-        // Start background task
-        let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") { [weak self] in
-            guard let self, let backgroundTask = self.backgroundTaskID else { return }
-            Task {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
+            // End the background task
+            if taskID != .invalid {
+                await UIApplication.shared.endBackgroundTask(taskID)
+                taskID = .invalid
             }
-            self.backgroundTaskID = .invalid
         }
-        backgroundTaskID = backgroundTask
-
-        // Set loop start time
-        lastLoopStartDate = Date()
-
-        // Calculate interval from previous loop
-        let interval = await calculateLoopInterval()
-
-        // Create initial loop stats record
-        let loopStatRecord = LoopStats(
-            start: lastLoopStartDate,
-            loopStatus: "Starting",
-            interval: interval
-        )
-
-        isLooping.send(true)
-
-        return (loopStatRecord, backgroundTask)
     }
 
     private func executeLoop(loopStatRecord: inout LoopStats) async throws {
         try await determineBasal()
 
-        // Handle open loop
-        guard settings.closedLoop else {
-            loopStatRecord.end = Date()
-            loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-            loopStatRecord.loopStatus = "Success"
-            await loopCompleted(loopStatRecord: loopStatRecord)
-            return
+        // Closed loop: also enact the determination.
+        if settings.closedLoop {
+            try await enactDetermination()
         }
 
-        // Handle closed loop
-        try await enactDetermination()
-        loopStatRecord.end = Date()
-        loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+        let endDate = Date()
+        loopStatRecord.end = endDate
+        loopStatRecord.duration = roundDouble((endDate - loopStatRecord.start).timeInterval / 60, 2)
         loopStatRecord.loopStatus = "Success"
-        await loopCompleted(loopStatRecord: loopStatRecord)
     }
 
-    private func calculateLoopInterval() async -> Double? {
+    private func calculateLoopInterval(loopStartDate: Date) async -> Double? {
         do {
-            return try await privateContext.perform {
+            return try await privateContext.perform { [weak self] in
+                guard let self else { return nil }
                 let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
                 let sortStats = NSSortDescriptor(key: "end", ascending: false)
                 requestStats.sortDescriptors = [sortStats]
                 requestStats.fetchLimit = 1
                 let previousLoop = try self.privateContext.fetch(requestStats)
 
-                if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
+                if (previousLoop.first?.end ?? .distantFuture) < loopStartDate {
                     return self.roundDouble(
-                        (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
+                        (loopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
                         1
                     )
                 }
@@ -354,16 +358,13 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    // Loop exit point
-    private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
+    /// Single exit point for loop — replaces the old `loopCompleted()`.
+    private func finalizeLoop(error: Error? = nil, loopStatRecord: LoopStats) async {
+        await loopGuard.finish()
         isLooping.send(false)
 
         if let error = error {
             warning(.apsManager, "Loop failed with error: \(error)")
-            if let backgroundTask = backgroundTaskID {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
-                backgroundTaskID = .invalid
-            }
             processError(error)
         } else {
             debug(.apsManager, "Loop succeeded")
@@ -376,12 +377,6 @@ final class BaseAPSManager: APSManager, Injectable {
         if settings.closedLoop {
             await reportEnacted(wasEnacted: error == nil)
         }
-
-        // End of the BG tasks
-        if let backgroundTask = backgroundTaskID {
-            await UIApplication.shared.endBackgroundTask(backgroundTask)
-            backgroundTaskID = .invalid
-        }
     }
 
     private func verifyStatus() -> Error? {
@@ -478,14 +473,13 @@ final class BaseAPSManager: APSManager, Injectable {
         do {
             let now = Date()
 
-            // Parallelize the fetches using async let
-            async let currentTemp = fetchCurrentTempBasal(date: now)
-            async let autosenseResult = autosense()
-
-            _ = try await autosenseResult
+            // put profile creation up front since autosens needs it
             try await openAPS.createProfiles(useSwiftOref: settings.useSwiftOref)
+            let currentTemp = try await fetchCurrentTempBasal(date: now)
+            _ = try await autosense()
+
             let determination = try await openAPS.determineBasal(
-                currentTemp: await currentTemp,
+                currentTemp: currentTemp,
                 shouldSmoothGlucose: settingsManager.settings.smoothGlucose,
                 useSwiftOref: settings.useSwiftOref,
                 clock: now
@@ -556,8 +550,6 @@ final class BaseAPSManager: APSManager, Injectable {
         return min(rounded, maxBolus)
     }
 
-    private var bolusReporter: DoseProgressReporter?
-
     func enactBolus(amount: Double, isSMB: Bool, callback: ((Bool, String) -> Void)?) async {
         if amount <= 0 {
             return
@@ -637,9 +629,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 )
             )
         }
-        bolusReporter?.removeObserver(self)
-        bolusReporter = nil
-        bolusProgress.send(nil)
+        clearBolusReporter()
     }
 
     func enactTempBasal(rate: Double, duration: TimeInterval) async {
@@ -838,31 +828,31 @@ final class BaseAPSManager: APSManager, Injectable {
         return Double(sorted[length / 2])
     }
 
+    /// Computes Time-in-Range statistics. Must be called from inside a
+    /// `privateContext.perform` block — the inner perform was redundant
     private func tir(_ glucose: [GlucoseStored]) -> (TIR: Double, hypos: Double, hypers: Double, normal_: Double) {
-        privateContext.perform {
-            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-            let totalReadings = justGlucoseArray.count
-            let highLimit = settingsManager.settings.high
-            let lowLimit = settingsManager.settings.low
-            let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
-            let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
-            let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
-            let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
-            let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
-            let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
-            // Euglyccemic range
-            let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
-            let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
-            let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
-            // TIR
-            let tir = 100 - (hypoPercentage + hyperPercentage)
-            return (
-                roundDouble(tir, 1),
-                roundDouble(hypoPercentage, 1),
-                roundDouble(hyperPercentage, 1),
-                roundDouble(normalPercentage, 1)
-            )
-        }
+        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+        let totalReadings = justGlucoseArray.count
+        let highLimit = settingsManager.settings.high
+        let lowLimit = settingsManager.settings.low
+        let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
+        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
+        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
+        let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
+        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
+        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
+        // Euglycemic range
+        let normalArray = glucose.filter({ $0.glucose >= 70 && $0.glucose <= 140 })
+        let normalReadings = normalArray.compactMap({ each in each.glucose as Int16 }).count
+        let normalPercentage = Double(normalReadings) / Double(totalReadings) * 100
+        // TIR
+        let tir = 100 - (hypoPercentage + hyperPercentage)
+        return (
+            roundDouble(tir, 1),
+            roundDouble(hypoPercentage, 1),
+            roundDouble(hyperPercentage, 1),
+            roundDouble(normalPercentage, 1)
+        )
     }
 
     private func glucoseStats(_ fetchedGlucose: [GlucoseStored])
@@ -1196,7 +1186,8 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func loopStats(loopStatRecord: LoopStats) {
-        privateContext.perform {
+        privateContext.perform { [weak self] in
+            guard let self else { return }
             let nLS = LoopStatRecord(context: self.privateContext)
             nLS.start = loopStatRecord.start
             nLS.end = loopStatRecord.end ?? Date()
@@ -1218,15 +1209,22 @@ final class BaseAPSManager: APSManager, Injectable {
         lastError.send(error)
     }
 
+    /// Called from the `bolusTrigger` Combine sink (already on
+    /// `processQueue`) and from `doseProgressReporterDidUpdate` (the
+    /// pump manager schedules the callback on `processQueue` too).
+    /// Mutations are dispatched onto the queue regardless, so a future
+    /// caller from another context (e.g. `cancelBolus`) stays safe.
     private func createBolusReporter() {
-        bolusReporter = pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
-        bolusReporter?.addObserver(self)
+        processQueue.async {
+            self.bolusReporter = self.pumpManager?.createBolusProgressReporter(reportingOn: self.processQueue)
+            self.bolusReporter?.addObserver(self)
+        }
     }
 
     private func clearBolusReporter() {
-        bolusReporter?.removeObserver(self)
-        bolusReporter = nil
-        processQueue.asyncAfter(deadline: .now() + 0.5) {
+        processQueue.async {
+            self.bolusReporter?.removeObserver(self)
+            self.bolusReporter = nil
             self.bolusProgress.send(nil)
         }
     }
@@ -1307,7 +1305,8 @@ extension BaseAPSManager: PumpManagerStatusObserver {
     func pumpManager(_: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
         let percent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
 
-        privateContext.perform {
+        privateContext.perform { [weak self] in
+            guard let self else { return }
             /// only update the last item with the current battery infos instead of saving a new one each time
             let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
             fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]

+ 98 - 1
Trio/Sources/APS/CGM/AppGroupSource.swift

@@ -30,6 +30,9 @@ struct AppGroupSource: GlucoseSource {
     let from: String
     var cgmType: CGMType
 
+    let cgmDisplayState = CurrentValueSubject<CgmDisplayState?, Never>(nil)
+    let cgmProgressHighlight = CurrentValueSubject<LoopKit.DeviceLifecycleProgress?, Never>(nil)
+
     func fetch(_ heartbeat: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         guard let suiteName = Bundle.main.appGroupSuiteName,
               let sharedDefaults = UserDefaults(suiteName: suiteName)
@@ -52,7 +55,21 @@ struct AppGroupSource: GlucoseSource {
         HeartBeatManager.shared.checkCGMBluetoothTransmitter(sharedUserDefaults: sharedDefaults, heartbeat: heartbeat)
         debug(.deviceManager, "APPGROUP : START FETCH LAST BG ")
         let decoded = try? JSONSerialization.jsonObject(with: sharedData, options: [])
-        guard let sgvs = decoded as? [AnyObject] else {
+
+        // Two shapes accepted:
+        //   Legacy (xDrip4iOS today): top-level array of reading dicts.
+        //   Rich (xDrip4iOS extended for CGM lifecycle):
+        //   top-level dict carrying readings under
+        //   `recentReadings` plus sibling keys for CGM status, sensor
+        //   lifecycle, and transmitter info — see `applyRichState`.
+        let sgvs: [AnyObject]
+        if let dict = decoded as? [String: Any] {
+            applyRichState(dict)
+            sgvs = (dict["recentReadings"] as? [AnyObject]) ?? []
+        } else if let arr = decoded as? [AnyObject] {
+            applyRichState(nil)
+            sgvs = arr
+        } else {
             return []
         }
 
@@ -101,6 +118,81 @@ struct AppGroupSource: GlucoseSource {
         return results
     }
 
+    /// Reads the rich top-level dict from xDrip4iOS (when present) and
+    /// pushes status + lifecycle into the publishers HomeStateModel
+    /// subscribes to. Defensive on every key — xdrip ships partial dicts
+    /// during warmup / failure / between sensors.
+    private func applyRichState(_ payload: [String: Any]?) {
+        guard let payload else {
+            cgmDisplayState.value = nil
+            cgmProgressHighlight.value = nil
+            return
+        }
+
+        let cgm = payload["cgm"] as? [String: Any]
+        cgmDisplayState.value = parseStatus(cgm?["status"] as? [String: Any])
+        cgmProgressHighlight.value = parseSensorLifecycle(cgm?["sensor"] as? [String: Any])
+    }
+
+    private func parseStatus(_ status: [String: Any]?) -> CgmDisplayState? {
+        guard let status,
+              let message = status["localizedMessage"] as? String,
+              !message.isEmpty
+        else { return nil }
+        return CgmDisplayState(
+            localizedMessage: message,
+            imageName: (status["imageName"] as? String) ?? "",
+            status: cgmDisplayStatus(forCode: status["displayState"] as? String ?? status["code"] as? String)
+        )
+    }
+
+    /// xDrip4iOS sends a free-form code string (e.g. "normal", "warning",
+    /// "critical", "warmup", "calibration_needed", "sensor_failed"). We
+    /// fold anything unfamiliar into `.warning` so unknown future codes
+    /// surface visibly instead of going silent.
+    private func cgmDisplayStatus(forCode code: String?) -> CgmDisplayStatus {
+        switch code?.lowercased() {
+        case nil,
+             "normal",
+             "ok": return .normal
+        case "critical",
+             "expired",
+             "sensor_failed",
+             "session_failed",
+             "stopped": return .critical
+        default: return .warning
+        }
+    }
+
+    private func parseSensorLifecycle(_ sensor: [String: Any]?) -> DeviceLifecycleProgress? {
+        guard let sensor else { return nil }
+        let percent = (sensor["percentComplete"] as? NSNumber)?.doubleValue
+        guard let percent else { return nil }
+        let progressState = lifecycleProgressState(
+            for: sensor["progressState"] as? String,
+            isInWarmup: sensor["isInWarmup"] as? Bool ?? false,
+            isExpired: sensor["isExpired"] as? Bool ?? false
+        )
+        return AppGroupLifecycleProgress(
+            percentComplete: max(0, min(1, percent)),
+            progressState: progressState
+        )
+    }
+
+    private func lifecycleProgressState(
+        for code: String?,
+        isInWarmup: Bool,
+        isExpired: Bool
+    ) -> DeviceLifecycleProgressState {
+        if isExpired { return .critical }
+        if isInWarmup { return .normalCGM }
+        switch code?.lowercased() {
+        case "critical": return .critical
+        case "warning": return .warning
+        default: return .normalCGM
+        }
+    }
+
     private func parseDate(_ timestamp: String) -> Date? {
         // timestamp looks like "/Date(1462404576000)/"
         guard let re = try? NSRegularExpression(pattern: "\\((.*)\\)"),
@@ -119,6 +211,11 @@ struct AppGroupSource: GlucoseSource {
     }
 }
 
+private struct AppGroupLifecycleProgress: DeviceLifecycleProgress {
+    let percentComplete: Double
+    let progressState: DeviceLifecycleProgressState
+}
+
 public extension Bundle {
     var appGroupSuiteName: String? {
         object(forInfoDictionaryKey: "AppGroupID") as? String

+ 228 - 0
Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -13,7 +13,9 @@
 ///  - OscillatingGenerator: BloodGlucoseGenerator - Generates sinusoidal glucose values around a center point
 
 import Combine
+import CoreData
 import Foundation
+import LoopKit
 import LoopKitUI
 
 // MARK: - Glucose simulator
@@ -25,6 +27,9 @@ final class GlucoseSimulatorSource: GlucoseSource {
     var cgmManager: CGMManagerUI?
     var glucoseManager: FetchGlucoseManager?
 
+    let cgmDisplayState = CurrentValueSubject<CgmDisplayState?, Never>(nil)
+    let cgmProgressHighlight = CurrentValueSubject<DeviceLifecycleProgress?, Never>(nil)
+
     private enum Config {
         /// Minimum time period between data publications (in seconds)
         static let workInterval: TimeInterval = 300
@@ -49,6 +54,57 @@ final class GlucoseSimulatorSource: GlucoseSource {
             }
             lastFetchDate = lastDate
         }
+        publishSimulatedState()
+    }
+
+    /// Republishes synthetic lifecycle/highlight from `simulatedScenario`.
+    func publishSimulatedState() {
+        cgmProgressHighlight.value = cgmLifecycleProgress
+        if let highlight = cgmStatusHighlight {
+            cgmDisplayState.value = CgmDisplayState(
+                localizedMessage: highlight.localizedMessage,
+                imageName: highlight.imageName,
+                status: CgmDisplayStatus.from(highlight.state)
+            )
+        } else {
+            cgmDisplayState.value = nil
+        }
+    }
+
+    /// Picker entry point — change scenario + propagate immediately. When
+    /// flipping to a state where a real sensor wouldn't be delivering fresh
+    /// readings, drop any GlucoseStored rows inside the home view's 12 min
+    /// freshness window so the bobble switches to its compact stale view
+    /// right away instead of waiting for organic aging.
+    func applySimulatedScenario(_ scenario: SimulatedSensorScenario) {
+        simulatedScenario = scenario
+        if !scenario.deliversFreshGlucose {
+            clearRecentSimulatorReadings()
+        }
+        publishSimulatedState()
+    }
+
+    /// Deletes GlucoseStored rows newer than the home view's 12 min
+    /// freshness window. Dev-only — only invoked from the simulator's
+    /// scenario picker, which is itself gated to simulator mode.
+    private func clearRecentSimulatorReadings() {
+        let context = CoreDataStack.shared.newTaskContext()
+        let cutoff = Date().addingTimeInterval(-12 * 60)
+        context.perform {
+            let request = GlucoseStored.fetchRequest()
+            request.predicate = NSPredicate(format: "date > %@", cutoff as NSDate)
+            do {
+                let recent = try context.fetch(request)
+                for row in recent {
+                    context.delete(row)
+                }
+                if context.hasChanges {
+                    try context.save()
+                }
+            } catch {
+                print("GlucoseSimulatorSource: clearRecentSimulatorReadings failed: \(error)")
+            }
+        }
     }
 
     /// The glucose generator used to create simulated values
@@ -74,6 +130,14 @@ final class GlucoseSimulatorSource: GlucoseSource {
         guard canGenerateNewValues else {
             return Just([]).eraseToAnyPublisher()
         }
+        // Match real CGM behavior: scenarios where a physical sensor wouldn't
+        // be delivering readings (warmup, calibration, expired, failed) also
+        // stop the simulator from emitting fresh values. Existing readings
+        // then age out of the 12 min freshness window, the bobble flips to
+        // its compact symbol view, and the highlight's imageName surfaces.
+        guard simulatedScenario.deliversFreshGlucose else {
+            return Just([]).eraseToAnyPublisher()
+        }
 
         let glucoses = generator.getBloodGlucoses(
             startDate: lastFetchDate,
@@ -94,6 +158,170 @@ final class GlucoseSimulatorSource: GlucoseSource {
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
         fetch(nil)
     }
+
+    // MARK: - Simulated sensor lifecycle / status (dev-only)
+
+    //
+    // The simulator doesn't own a real `CGMManagerUI`, so `cgmStatusHighlight`
+    // and `cgmLifecycleProgress` would be nil on the home screen. To make the
+    // outer arc + tag indicator actually exercisable without Libre/Dexcom
+    // hardware, the simulator exposes synthetic values driven by
+    // `simulatedScenario`. Flip the persisted enum at runtime (debug menu or
+    // by editing the default below) and the home view picks up the change on
+    // its next 5-second refresh tick.
+
+    /// Which pre-canned sensor state the simulator should advertise. Plain
+    /// `UserDefaults` string access (not `@Persisted`) so the CGM-settings
+    /// picker — which writes via `UserDefaults.standard.set(_:forKey:)` — and
+    /// the read here use the same encoding. `@Persisted` stores values as
+    /// JSON-wrapped `Data`, which would silently break the picker round-trip.
+    static let simulatedScenarioKey = "GlucoseSimulator.simulatedScenario"
+
+    var simulatedScenario: SimulatedSensorScenario {
+        get {
+            let raw = UserDefaults.standard.string(forKey: Self.simulatedScenarioKey)
+            return raw.flatMap(SimulatedSensorScenario.init(rawValue:)) ?? .runningNormally
+        }
+        set {
+            UserDefaults.standard.set(newValue.rawValue, forKey: Self.simulatedScenarioKey)
+        }
+    }
+
+    /// Synthetic expiration date that mirrors what a real CGM would expose.
+    /// Picked so the remaining-time label matches the scenario's
+    /// `percentComplete` against a 10-day total (Dexcom-like).
+    var simulatedSensorExpiresAt: Date? {
+        guard let progress = cgmLifecycleProgress else { return nil }
+        let totalLifetime: TimeInterval = 10 * 24 * 60 * 60
+        let remaining = (1.0 - progress.percentComplete) * totalLifetime
+        return Date().addingTimeInterval(remaining)
+    }
+
+    /// Synthetic outer-arc data — nil for scenarios where lifetime is moot
+    /// (warmup, hardware fault). Matches the production `DeviceLifecycleProgress`
+    /// shape so the home state model can treat both sources identically.
+    var cgmLifecycleProgress: DeviceLifecycleProgress? {
+        switch simulatedScenario {
+        case .runningNormally:
+            return SimulatedLifecycleProgress(percentComplete: 0.45, progressState: .normalCGM)
+        case .expiringSoon:
+            return SimulatedLifecycleProgress(percentComplete: 0.94, progressState: .warning)
+        case .warmup:
+            return nil
+        case .calibrationRequired:
+            return SimulatedLifecycleProgress(percentComplete: 0.30, progressState: .normalCGM)
+        case .expired:
+            return SimulatedLifecycleProgress(percentComplete: 1.0, progressState: .critical)
+        case .sensorFailed:
+            return nil
+        }
+    }
+
+    /// Synthetic status highlight. nil for `.runningNormally` and
+    /// `.expiringSoon` (those run off lifecycle only).
+    var cgmStatusHighlight: DeviceStatusHighlight? {
+        switch simulatedScenario {
+        case .expiringSoon,
+             .runningNormally:
+            return nil
+        case .warmup:
+            return SimulatedStatusHighlight(
+                localizedMessage: "Sensor warming up",
+                imageName: "hourglass",
+                state: .warning
+            )
+        case .calibrationRequired:
+            return SimulatedStatusHighlight(
+                localizedMessage: "Calibrate",
+                imageName: "drop.fill",
+                state: .warning
+            )
+        case .expired:
+            return SimulatedStatusHighlight(
+                localizedMessage: "Sensor expired",
+                imageName: "exclamationmark.circle.fill",
+                state: .critical
+            )
+        case .sensorFailed:
+            return SimulatedStatusHighlight(
+                localizedMessage: "Replace Sensor",
+                imageName: "exclamationmark.triangle.fill",
+                state: .critical
+            )
+        }
+    }
+}
+
+/// Pre-canned sensor scenarios surfaced by `GlucoseSimulatorSource`. One per
+/// home-screen state so flipping this drives the indicator
+/// through every visual state.
+enum SimulatedSensorScenario: String, CaseIterable, Identifiable {
+    case runningNormally
+    case expiringSoon
+    case warmup
+    case calibrationRequired
+    case expired
+    case sensorFailed
+
+    var id: String { rawValue }
+
+    var displayName: String {
+        switch self {
+        case .runningNormally: return "Running normally"
+        case .expiringSoon: return "Expiring soon"
+        case .warmup: return "Warmup"
+        case .calibrationRequired: return "Calibration required"
+        case .expired: return "Expired"
+        case .sensorFailed: return "Sensor failed"
+        }
+    }
+
+    /// Whether a real CGM would still be delivering fresh glucose readings
+    /// while in this state. Drives the simulator's `fetch()` gate so non-
+    /// active scenarios stop emitting and the home view sees stale data
+    /// the same way it would from a real sensor.
+    var deliversFreshGlucose: Bool {
+        switch self {
+        case .expiringSoon,
+             .runningNormally:
+            return true
+        case .calibrationRequired,
+             .expired,
+             .sensorFailed,
+             .warmup:
+            return false
+        }
+    }
+
+    /// Short blurb shown under the picker so dev users know what each
+    /// scenario renders on the home screen.
+    var devNotes: String {
+        switch self {
+        case .runningNormally:
+            return "Teal outer ring at ~45%, tag shows time remaining."
+        case .expiringSoon:
+            return "Amber outer ring at ~94%, tag shows time remaining with \"left\" suffix."
+        case .warmup:
+            return "Arc hidden, pulsing amber tag, glucose shown as \"– –\"."
+        case .calibrationRequired:
+            return "Arc visible, amber tag with \"Calibrate\" message, glucose still shown."
+        case .expired:
+            return "Red full ring, red tag \"sensor expired\", glucose shown as \"– –\"."
+        case .sensorFailed:
+            return "Arc hidden, pulsing red tag, glucose shown as \"– –\"."
+        }
+    }
+}
+
+private struct SimulatedLifecycleProgress: DeviceLifecycleProgress {
+    let percentComplete: Double
+    let progressState: DeviceLifecycleProgressState
+}
+
+private struct SimulatedStatusHighlight: DeviceStatusHighlight {
+    let localizedMessage: String
+    let imageName: String
+    let state: DeviceStatusHighlightState
 }
 
 // MARK: - Glucose generator

+ 4 - 0
Trio/Sources/APS/CGM/GlucoseSource.swift

@@ -11,6 +11,10 @@ protocol GlucoseSource: SourceInfoProvider {
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never>
     var glucoseManager: FetchGlucoseManager? { get set }
     var cgmManager: CGMManagerUI? { get set }
+    /// Mirrors `CGMManagerUI.cgmStatusHighlight`; republished on manager change.
+    var cgmDisplayState: CurrentValueSubject<CgmDisplayState?, Never> { get }
+    /// Mirrors `CGMManagerUI.cgmLifecycleProgress`.
+    var cgmProgressHighlight: CurrentValueSubject<LoopKit.DeviceLifecycleProgress?, Never> { get }
 }
 
 extension GlucoseSource {

+ 27 - 1
Trio/Sources/APS/CGM/PluginSource.swift

@@ -11,7 +11,12 @@ final class PluginSource: GlucoseSource {
     private let glucoseStorage: GlucoseStorage!
     var glucoseManager: FetchGlucoseManager?
 
-    var cgmManager: CGMManagerUI?
+    let cgmDisplayState = CurrentValueSubject<CgmDisplayState?, Never>(nil)
+    let cgmProgressHighlight = CurrentValueSubject<DeviceLifecycleProgress?, Never>(nil)
+
+    var cgmManager: CGMManagerUI? {
+        didSet { publishCGMStatus() }
+    }
 
     var cgmHasValidSensorSession: Bool = false
 
@@ -22,6 +27,23 @@ final class PluginSource: GlucoseSource {
         cgmManager = glucoseManager.cgmManager
         cgmManager?.delegateQueue = processQueue
         cgmManager?.cgmManagerDelegate = self
+        // didSet doesn't fire from the defining class's own init.
+        publishCGMStatus()
+    }
+
+    /// Republishes the manager's lifecycle/highlight to the subjects.
+    /// Called from `cgmManager.didSet` and after delegate state updates.
+    func publishCGMStatus() {
+        if let highlight = cgmManager?.cgmStatusHighlight {
+            cgmDisplayState.value = CgmDisplayState(
+                localizedMessage: highlight.localizedMessage,
+                imageName: highlight.imageName,
+                status: CgmDisplayStatus.from(highlight.state)
+            )
+        } else {
+            cgmDisplayState.value = nil
+        }
+        cgmProgressHighlight.value = cgmManager?.cgmLifecycleProgress
     }
 
     /// Function that fetches blood glucose data
@@ -121,6 +143,8 @@ extension PluginSource: CGMManagerDelegate {
                 debug(.deviceManager, "CGM PLUGIN - unable to read CGM result")
             }
 
+            self.publishCGMStatus()
+
             debug(.deviceManager, "CGM PLUGIN - Direct return done")
         }
     }
@@ -170,6 +194,8 @@ extension PluginSource: CGMManagerDelegate {
                 cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
                 newManager: cgmManager as? CGMManagerUI
             )
+
+            self.publishCGMStatus()
         }
     }
 

+ 10 - 12
Trio/Sources/APS/DeviceDataManager.swift

@@ -341,6 +341,9 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     var cgmManager: CGMManagerUI?
     var cgmType: CGMType = .enlite
 
+    let cgmDisplayState = CurrentValueSubject<CgmDisplayState?, Never>(nil)
+    let cgmProgressHighlight = CurrentValueSubject<DeviceLifecycleProgress?, Never>(nil)
+
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
         fetch(nil)
     }
@@ -487,6 +490,13 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             settingsManager.updateInsulinCurve(status.insulinType)
         }
 
+        // Check if manual temp basal is active
+        let manualTempBasalActive = status.basalDeliveryState?.isManualTempBasal ?? false
+        if manualTempBasalActive {
+            debug(.deviceManager, "manual temp basal")
+        }
+        manualTempBasal.send(manualTempBasalActive)
+
         if let medtrumPump = pumpManager as? MedtrumPumpManager {
             storage.save(Decimal(medtrumPump.state.reservoir), as: OpenAPS.Monitor.reservoir)
             broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
@@ -520,18 +530,6 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
                 $0.pumpReservoirDidChange(Decimal(reservoir))
             }
 
-            // manual temp basal on
-            if let tempBasal = omni.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(),
-               !tempBasal.automatic
-            {
-                // the manual basal temp is launch - block every thing
-                debug(.deviceManager, "manual temp basal")
-                manualTempBasal.send(true)
-            } else {
-                // no more manual Temp Basal !
-                manualTempBasal.send(false)
-            }
-
             guard let endTime = omni.state.podState?.expiresAt else {
                 pumpExpiresAtDate.send(nil)
                 return

+ 23 - 1
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -19,6 +19,8 @@ protocol FetchGlucoseManager: SourceInfoProvider {
     var cgmGlucosePluginId: String { get }
     var settingsManager: SettingsManager! { get }
     var shouldSyncToRemoteService: Bool { get }
+    var cgmDisplayState: CurrentValueSubject<CgmDisplayState?, Never> { get }
+    var cgmProgressHighlight: CurrentValueSubject<DeviceLifecycleProgress?, Never> { get }
 }
 
 extension FetchGlucoseManager {
@@ -148,7 +150,27 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
     }
 
-    var glucoseSource: GlucoseSource?
+    let cgmDisplayState = CurrentValueSubject<CgmDisplayState?, Never>(nil)
+    let cgmProgressHighlight = CurrentValueSubject<DeviceLifecycleProgress?, Never>(nil)
+    private var cgmStatusSubscriptions = Set<AnyCancellable>()
+
+    var glucoseSource: GlucoseSource? {
+        didSet {
+            // Drop prior subscriptions so source swaps don't dupe emissions.
+            cgmStatusSubscriptions.removeAll()
+            cgmDisplayState.value = glucoseSource?.cgmDisplayState.value
+            cgmProgressHighlight.value = glucoseSource?.cgmProgressHighlight.value
+            guard let glucoseSource else { return }
+            glucoseSource.cgmDisplayState
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] state in self?.cgmDisplayState.value = state }
+                .store(in: &cgmStatusSubscriptions)
+            glucoseSource.cgmProgressHighlight
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] progress in self?.cgmProgressHighlight.value = progress }
+                .store(in: &cgmStatusSubscriptions)
+        }
+    }
 
     func removeCalibrations() {
         calibrationService.removeAllCalibrations()

+ 8 - 0
Trio/Sources/Helpers/BasalDeliveryState+Extension.swift

@@ -0,0 +1,8 @@
+import LoopKit
+
+extension PumpManagerStatus.BasalDeliveryState {
+    var isManualTempBasal: Bool {
+        guard case let .tempBasal(dose) = self else { return false }
+        return !(dose.automatic ?? true)
+    }
+}

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

@@ -4822,6 +4822,9 @@
         }
       }
     },
+    "– –" : {
+
+    },
     ", %lld%%" : {
       "localizations" : {
         "bg" : {
@@ -101111,6 +101114,9 @@
         }
       }
     },
+    "Drives the outer-ring + tag on the home screen's glucose bobble." : {
+
+    },
     "Due to how the curve is calculated when using the Sigmoid Formula, increasing this setting has a different impact on the steepness of the curve than in the standard logarithmic Dynamic ISF calculation. Use caution when adjusting this setting." : {
       "localizations" : {
         "bg" : {
@@ -214592,6 +214598,9 @@
         }
       }
     },
+    "Scenario" : {
+
+    },
     "Schedule " : {
       "extractionState" : "manual",
       "localizations" : {
@@ -219782,6 +219791,9 @@
         }
       }
     },
+    "Sensor Lifecycle Scenario" : {
+
+    },
     "Sensor might have temporarily stopped, fallen off or is too cold or too warm" : {
       "comment" : "ensor might have temporarily stopped, fallen off or is too cold or too warm",
       "extractionState" : "manual",

+ 39 - 0
Trio/Sources/Modules/CGMSettings/CGMSettingsDataFlow.swift

@@ -1,5 +1,44 @@
+import LoopKit
+import SwiftUI
+
 enum CGMSettings {
     enum Config {}
 }
 
 protocol CGMSettingsProvider: Provider {}
+
+/// `cgmStatusHighlight` reduced to message + tier — what the home UI needs.
+/// `imageName` is the SF Symbol name the manager wants to associate with this
+/// state (G6/G7/LibreLoop all surface this); empty string when the manager
+/// doesn't provide one.
+struct CgmDisplayState: Equatable {
+    let localizedMessage: String
+    let imageName: String
+    let status: CgmDisplayStatus
+}
+
+enum CgmDisplayStatus {
+    case normal
+    case warning
+    case critical
+
+    var color: Color {
+        switch self {
+        case .critical: return .critical
+        case .warning: return .warning
+        case .normal: return .loopAccent
+        }
+    }
+
+    static func from(_ state: LoopKit.DeviceStatusHighlightState) -> CgmDisplayStatus {
+        switch state {
+        case .normalCGM,
+             .normalPump:
+            return .normal
+        case .warning:
+            return .warning
+        case .critical:
+            return .critical
+        }
+    }
+}

+ 35 - 0
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -22,6 +22,10 @@ extension CGMSettings {
         @State private var noiseAmplitude: Double = UserDefaults.standard.double(forKey: "GlucoseSimulator_NoiseAmplitude")
         @State private var produceStaleValues: Bool = UserDefaults.standard.bool(forKey: "GlucoseSimulator_ProduceStaleValues")
 
+        /// Drives the synthetic `cgmStatusHighlight`
+        @State private var simulatedScenarioRaw: String = UserDefaults.standard
+            .string(forKey: "GlucoseSimulator.simulatedScenario") ?? SimulatedSensorScenario.runningNormally.rawValue
+
         // Initialize state variables with defaults if needed
         private func initializeSimulatorSettings() {
             if centerValue == 0 {
@@ -261,6 +265,37 @@ extension CGMSettings {
                     }
                 }.listRowBackground(Color.chart)
 
+                Section(
+                    header: Text("Sensor Lifecycle Scenario"),
+                    footer: Text(
+                        "Drives the outer-ring + tag on the home screen's glucose bobble."
+                    )
+                ) {
+                    Picker("Scenario", selection: $simulatedScenarioRaw) {
+                        ForEach(SimulatedSensorScenario.allCases) { scenario in
+                            Text(scenario.displayName).tag(scenario.rawValue)
+                        }
+                    }
+                    .pickerStyle(.menu)
+                    .onChange(of: simulatedScenarioRaw) { _, newValue in
+                        UserDefaults.standard.set(newValue, forKey: "GlucoseSimulator.simulatedScenario")
+                        // Push the change through the active simulator
+                        // instance so subjects emit immediately.
+                        if let scenario = SimulatedSensorScenario(rawValue: newValue),
+                           let sim = resolver.resolve(FetchGlucoseManager.self)?.glucoseSource as? GlucoseSimulatorSource
+                        {
+                            sim.applySimulatedScenario(scenario)
+                        }
+                    }
+
+                    if let scenario = SimulatedSensorScenario(rawValue: simulatedScenarioRaw) {
+                        Text(scenario.devNotes)
+                            .font(.footnote)
+                            .foregroundStyle(Color.secondary)
+                            .lineLimit(nil)
+                    }
+                }.listRowBackground(Color.chart)
+
                 if !produceStaleValues {
                     Section {
                         VStack(alignment: .leading, spacing: 10) {

+ 128 - 2
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -1,7 +1,10 @@
+import CGMBLEKit
 import CGMBLEKitUI
 import Combine
 import CoreData
 import Foundation
+import G7SensorKit
+import LibreTransmitter
 import LoopKit
 import LoopKitUI
 import Observation
@@ -27,7 +30,7 @@ extension Home {
             CGMSettings.StateModel.shared
         }
 
-        private let timer = DispatchTimer(timeInterval: 5)
+        private let timer = DispatchTimer(timeInterval: 30)
         private(set) var filteredHours = 24
         var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
         var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
@@ -107,6 +110,10 @@ extension Home {
         var pumpStatusBadgeImage: UIImage?
         var pumpStatusBadgeColor: Color?
         var cgmAvailable: Bool = false
+        var cgmDisplayState: CgmDisplayState?
+        var cgmProgressHighlight: DeviceLifecycleProgress?
+        var cgmSensorExpiresAt: Date?
+        var cgmWarmupEndsAt: Date?
         var listOfCGM: [CGMModel] = []
         var cgmCurrent = cgmDefaultModel
         var pumpInitialSettings = PumpConfig.PumpInitialSettings.default
@@ -321,11 +328,68 @@ extension Home {
 
             timer.eventHandler = {
                 DispatchQueue.main.async { [weak self] in
-                    self?.timerDate = Date()
+                    guard let self else { return }
+                    self.timerDate = Date()
+                    // The publisher only re-emits on state changes; re-pull
+                    // so the arc + countdowns + status text advance during
+                    // warmup / stabilizing / expiry. Simulator has no
+                    // CGMManager, so fall back to reading its synthetic
+                    // lifecycle / highlight so the bobble sees the same
+                    // data shape a real CGM would deliver.
+                    let manager = self.fetchGlucoseManager.cgmManager
+                    let source = self.fetchGlucoseManager.glucoseSource
+                    let progress: DeviceLifecycleProgress?
+                    let highlight: DeviceStatusHighlight?
+                    if let manager {
+                        progress = manager.cgmLifecycleProgress
+                        highlight = manager.cgmStatusHighlight
+                    } else if let sim = source as? GlucoseSimulatorSource {
+                        progress = sim.cgmLifecycleProgress
+                        highlight = sim.cgmStatusHighlight
+                    } else {
+                        progress = nil
+                        highlight = nil
+                    }
+                    self.cgmProgressHighlight = progress
+                    if let highlight {
+                        self.cgmDisplayState = CgmDisplayState(
+                            localizedMessage: highlight.localizedMessage,
+                            imageName: highlight.imageName,
+                            status: CgmDisplayStatus.from(highlight.state)
+                        )
+                    } else {
+                        self.cgmDisplayState = nil
+                    }
+                    self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
+                        manager: manager,
+                        glucoseSource: source,
+                        lifecycle: progress
+                    )
+                    self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(manager: manager)
                 }
             }
             timer.resume()
 
+            fetchGlucoseManager.cgmDisplayState
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] state in self?.cgmDisplayState = state }
+                .store(in: &lifetime)
+            fetchGlucoseManager.cgmProgressHighlight
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] progress in
+                    guard let self else { return }
+                    self.cgmProgressHighlight = progress
+                    self.cgmSensorExpiresAt = Self.resolveSensorExpiresAt(
+                        manager: self.fetchGlucoseManager.cgmManager,
+                        glucoseSource: self.fetchGlucoseManager.glucoseSource,
+                        lifecycle: progress
+                    )
+                    self.cgmWarmupEndsAt = Self.resolveWarmupEndsAt(
+                        manager: self.fetchGlucoseManager.cgmManager
+                    )
+                }
+                .store(in: &lifetime)
+
             apsManager.isLooping
                 .receive(on: DispatchQueue.main)
                 .weakAssign(to: \.isLooping, on: self)
@@ -642,6 +706,68 @@ extension Home {
                 }
             }
         }
+
+        /// Sensor expiration for the home label. Prefers manager-reported
+        /// dates; reverse-derives from `lifecycle.percentComplete` when not.
+        /// `activatedAt` must be session start, not transmitter activation.
+        private static func resolveSensorExpiresAt(
+            manager: CGMManagerUI?,
+            glucoseSource: GlucoseSource?,
+            lifecycle: DeviceLifecycleProgress?
+        ) -> Date? {
+            if let sim = glucoseSource as? GlucoseSimulatorSource {
+                return sim.simulatedSensorExpiresAt
+            }
+            guard let manager else { return nil }
+            // Once a G7 enters grace period, `sensorExpiresAt` is in the past
+            // and would collapse the bobble countdown to "<1m" while the arc
+            // (driven by lifecycle.percentComplete against `sensorEndsAt`) is
+            // still mid-progress. Fall back to `sensorEndsAt` so bobble and
+            // arc agree, and the user sees grace-period time remaining.
+            if let g7 = manager as? G7CGMManager {
+                let now = Date()
+                if let exp = g7.sensorExpiresAt, exp > now { return exp }
+                return g7.sensorEndsAt ?? g7.sensorExpiresAt
+            }
+            if let g6 = manager as? G6CGMManager, let exp = g6.latestReading?.sessionExpDate { return exp }
+            if let g5 = manager as? G5CGMManager, let exp = g5.latestReading?.sessionExpDate { return exp }
+
+            let activatedAt: Date?
+            if let g7 = manager as? G7CGMManager {
+                activatedAt = g7.sensorActivatedAt
+            } else if let libre = manager as? LibreTransmitterManagerV3 {
+                activatedAt = libre.sensorInfoObservable.activatedAt
+            } else {
+                activatedAt = nil
+            }
+
+            guard let activatedAt,
+                  let lifecycle,
+                  lifecycle.percentComplete > 0.001
+            else { return nil }
+            let elapsed = Date().timeIntervalSince(activatedAt)
+            guard elapsed > 0 else { return nil }
+            return activatedAt.addingTimeInterval(elapsed / lifecycle.percentComplete)
+        }
+
+        /// Wall-clock end of the sensor's warmup window; `nil` when not warming up.
+        private static func resolveWarmupEndsAt(manager: CGMManagerUI?) -> Date? {
+            guard let manager else { return nil }
+            if let g7 = manager as? G7CGMManager {
+                guard let ends = g7.sensorFinishesWarmupAt, ends > Date() else { return nil }
+                return ends
+            }
+            if let g6 = manager as? G6CGMManager, let start = g6.latestReading?.sessionStartDate {
+                let window: TimeInterval = g6.isAnubis ? 50 * 60 : 2 * 60 * 60
+                let ends = start.addingTimeInterval(window)
+                return ends > Date() ? ends : nil
+            }
+            if let g5 = manager as? G5CGMManager, let start = g5.latestReading?.sessionStartDate {
+                let ends = start.addingTimeInterval(2 * 60 * 60)
+                return ends > Date() ? ends : nil
+            }
+            return nil
+        }
     }
 }
 

+ 233 - 72
Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -1,4 +1,5 @@
 import CoreData
+import LoopKit
 import SwiftUI
 
 struct CurrentGlucoseView: View {
@@ -11,6 +12,16 @@ struct CurrentGlucoseView: View {
     var currentGlucoseTarget: Decimal
     let glucoseColorScheme: GlucoseColorScheme
     let glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
+
+    /// Drives the outer ring.
+    var cgmProgress: DeviceLifecycleProgress?
+    /// CGM status highlight, rendered verbatim.
+    var cgmStatus: CgmDisplayState?
+    /// Sensor expiration — fallback tag when `cgmStatus` is nil.
+    var cgmSensorExpiresAt: Date?
+    /// Wall-clock end of the warmup window. Drives the warmup countdown tag.
+    var cgmWarmupEndsAt: Date?
+
     @State private var rotationDegrees: Double = 0.0
     @State private var angularGradient = AngularGradient(colors: [
         Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
@@ -42,81 +53,28 @@ struct CurrentGlucoseView: View {
         let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
 
         if cgmAvailable {
-            ZStack {
-                TrendShape(gradient: angularGradient, color: triangleColor)
-                    .rotationEffect(.degrees(rotationDegrees))
-
-                VStack(alignment: .center) {
+            if let stale = stalenessState {
+                // Compact error/transition state — same styles as the
+                // empty-state "Add CGM" layout below, just colored by tier.
+                VStack(alignment: .center, spacing: 12) {
                     HStack {
-                        if let glucoseValue = glucose.last?.glucose {
-                            let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
-                                .formattedAsMmolL
-
-                            var glucoseDisplayColor = Color.primary
-
-                            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
-                            let hardCodedLow = Decimal(55)
-                            let hardCodedHigh = Decimal(220)
-                            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
-
-                            if Decimal(glucoseValue) <= lowGlucose || Decimal(glucoseValue) >= highGlucose {
-                                glucoseDisplayColor = Trio.getDynamicGlucoseColor(
-                                    glucoseValue: Decimal(glucoseValue),
-                                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
-                                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
-                                    targetGlucose: currentGlucoseTarget,
-                                    glucoseColorScheme: glucoseColorScheme
-                                )
-                            }
-
-                            return Text(
-                                glucoseValue == 400 ? "HIGH" : displayGlucose
-                            )
-                            .font(.system(size: 40, weight: .bold, design: .rounded))
-                            .foregroundStyle(glucoseDisplayColor)
-                        } else {
-                            return Text("--")
-                                .font(.system(size: 40, weight: .bold, design: .rounded))
-                                .foregroundStyle(.secondary)
-                        }
+                        Image(systemName: stale.imageName)
+                            .font(.body)
+                            .imageScale(.large)
+                            .foregroundStyle(stale.color)
                     }
                     HStack {
-                        let minutesAgoString = TimeAgoFormatter.minutesAgo(from: glucose.last?.date)
-                        Group {
-                            Text(minutesAgoString)
-                            Text(delta)
-                        }
-                        .font(.callout).fontWeight(.bold)
-                        .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
-                    }
-                    .frame(alignment: .top)
-                }
-            }
-            .onChange(of: glucose.last?.directionEnum) {
-                withAnimation {
-                    switch glucose.last?.directionEnum {
-                    case .doubleUp,
-                         .singleUp,
-                         .tripleUp:
-                        rotationDegrees = -90
-                    case .fortyFiveUp:
-                        rotationDegrees = -45
-                    case .flat:
-                        rotationDegrees = 0
-                    case .fortyFiveDown:
-                        rotationDegrees = 45
-                    case .doubleDown,
-                         .singleDown,
-                         .tripleDown:
-                        rotationDegrees = 90
-                    case nil,
-                         .notComputable,
-                         .rateOutOfRange:
-                        rotationDegrees = 0
-                    default:
-                        rotationDegrees = 0
+                        Text(stale.label)
+                            .font(.caption).bold()
+                            .foregroundStyle(stale.color)
                     }
-                }
+                }.frame(alignment: .top)
+            } else {
+                // Bobble renders at 0.9 to leave breathing room for the right
+                // panel + pump view siblings; the compact and empty-state
+                // branches above and below already fit at 1.0.
+                bobbleAndTag(triangleColor: triangleColor)
+                    .scaleEffect(0.9)
             }
         } else {
             VStack(alignment: .center, spacing: 12) {
@@ -132,6 +90,59 @@ struct CurrentGlucoseView: View {
         }
     }
 
+    @ViewBuilder private func bobbleAndTag(triangleColor: Color) -> some View {
+        ZStack {
+            if let progress = cgmProgress, shouldShowArc {
+                SensorLifecycleArcView(
+                    progress: progress.percentComplete,
+                    progressState: progress.progressState
+                )
+            }
+
+            TrendShape(gradient: angularGradient, color: triangleColor, showArrow: true)
+                .rotationEffect(.degrees(rotationDegrees))
+
+            VStack(alignment: .center) {
+                bobbleContent()
+            }
+        }
+        .overlay(alignment: .bottom) {
+            // Overlay (not VStack) so the tag doesn't push siblings down;
+            // hidden when the trend arrow rotates toward 6 o'clock.
+            if let tag = tagLabel, !trendIsDownward {
+                SensorStatusTagView(text: tag.text, theme: tag.theme, iconSystemName: tag.icon)
+                    .offset(y: 14)
+                    .zIndex(1)
+            }
+        }
+        .onChange(of: glucose.last?.directionEnum) {
+            withAnimation {
+                switch glucose.last?.directionEnum {
+                case .doubleUp,
+                     .singleUp,
+                     .tripleUp:
+                    rotationDegrees = -90
+                case .fortyFiveUp:
+                    rotationDegrees = -45
+                case .flat:
+                    rotationDegrees = 0
+                case .fortyFiveDown:
+                    rotationDegrees = 45
+                case .doubleDown,
+                     .singleDown,
+                     .tripleDown:
+                    rotationDegrees = 90
+                case nil,
+                     .notComputable,
+                     .rateOutOfRange:
+                    rotationDegrees = 0
+                default:
+                    rotationDegrees = 0
+                }
+            }
+        }
+    }
+
     private var delta: String {
         guard glucose.count >= 2 else {
             return "--"
@@ -146,6 +157,153 @@ struct CurrentGlucoseView: View {
         let delta = lastGlucose - secondLastGlucose
         return deltaFormatter.string(from: delta as NSNumber) ?? "--"
     }
+
+    @ViewBuilder private func bobbleContent() -> some View {
+        HStack {
+            if let glucoseValue = glucose.last?.glucose, isReadingFresh {
+                let displayGlucose = units == .mgdL
+                    ? Decimal(glucoseValue).description
+                    : Decimal(glucoseValue).formattedAsMmolL
+                Text(glucoseValue == 400 ? "HIGH" : displayGlucose)
+                    .font(.system(size: 40, weight: .bold, design: .rounded))
+                    .foregroundStyle(glucoseColor(for: glucoseValue))
+            } else {
+                Text("– –")
+                    .font(.system(size: 40, weight: .bold, design: .rounded))
+                    .foregroundStyle(.secondary)
+            }
+        }
+        if isReadingFresh {
+            HStack {
+                let minutesAgoString = TimeAgoFormatter.minutesAgo(from: glucose.last?.date)
+                Group {
+                    Text(minutesAgoString)
+                    Text(delta)
+                }
+                .font(.callout).fontWeight(.bold)
+                .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
+            }
+            .frame(alignment: .top)
+        }
+    }
+
+    /// Matches `APSManager`'s loop-input freshness gate — readings older than
+    /// 12 minutes (one missed CGM transmission on a 5-min schedule) get
+    /// masked to dashes. Handles warmup + sensor failure naturally: no
+    /// fresh data → no number on the bobble.
+    private var isReadingFresh: Bool {
+        guard let date = glucose.last?.date else { return false }
+        return Date().timeIntervalSince(date) < 12 * 60
+    }
+
+    private var trendIsDownward: Bool { rotationDegrees >= 90 }
+
+    /// Error/transition states (sensor expired, sensor failure, signal loss,
+    /// stabilizing, etc.) collapse the bobble to a compact symbol-over-label
+    /// view — the bobble's purpose is to show glucose, so a fresh-reading-less
+    /// bobble with arc + dashes obscures rather than informs. Warmup is the
+    /// exception: it's a short, expected lifecycle phase and the arc + tag
+    /// countdown carry useful info, so the bobble stays.
+    private var stalenessState: (imageName: String, label: String, color: Color)? {
+        guard !isReadingFresh,
+              !isInWarmup,
+              let status = cgmStatus,
+              !status.imageName.isEmpty
+        else { return nil }
+        let color: Color
+        switch status.status {
+        case .critical: color = .loopRed
+        case .warning: color = .orange
+        case .normal: color = .secondary
+        }
+        // LibreLoop/G7 split labels with "\n" for their two-line native pills;
+        // collapse to spaces so the compact label reads cleanly.
+        let oneLine = status.localizedMessage.replacingOccurrences(of: "\n", with: " ")
+        return (status.imageName, oneLine, color)
+    }
+
+    /// Arc shown for warmup, the last 48 h of a time-based sensor (incl.
+    /// grace period), or any non-normal state from a battery-based manager.
+    private var shouldShowArc: Bool {
+        if isInWarmup { return true }
+        if let expiresAt = cgmSensorExpiresAt {
+            return expiresAt.timeIntervalSinceNow <= 48 * 60 * 60
+        }
+        return cgmProgress?.progressState != .normalCGM
+    }
+
+    /// String sniff — loopandlearn LoopKit has no structural warmup flag.
+    private var isInWarmup: Bool {
+        guard let message = cgmStatus?.localizedMessage else { return false }
+        let lowered = message.lowercased()
+        return lowered.contains("warming up") || lowered.contains("warmup")
+    }
+
+    private var isStabilizing: Bool {
+        cgmStatus?.localizedMessage.lowercased().contains("stabilizing") ?? false
+    }
+
+    /// Warmup → hourglass + countdown; stabilizing → hourglass + "Stabilizing"
+    /// (no countdown — duration is sensor-driven); outside warmup/stabilizing
+    /// the tag is gated to the same 48h window as the arc.
+    private var tagLabel: (text: String, theme: SensorStatusTagTheme, icon: String?)? {
+        if isInWarmup {
+            let text: String
+            if let endsAt = cgmWarmupEndsAt {
+                text = SensorRemainingTimeFormatter.format(until: endsAt)
+            } else {
+                text = "Warming up"
+            }
+            return (text, .orange, "hourglass")
+        }
+        if isStabilizing {
+            return ("Stabilizing", .orange, "hourglass")
+        }
+        guard shouldShowArc else { return nil }
+        if let status = cgmStatus {
+            // LibreLoop's cgmStatusHighlight uses "\n" to split two-line pill
+            // labels ("Signal\nLoss", "Sensor\nWarmup"); we render in a
+            // single-line tag, so collapse newlines to spaces here.
+            let oneLine = status.localizedMessage.replacingOccurrences(of: "\n", with: " ")
+            return (oneLine, theme(for: status.status), nil)
+        }
+        if let expiresAt = cgmSensorExpiresAt {
+            let text = SensorRemainingTimeFormatter.format(until: expiresAt)
+            let theme: SensorStatusTagTheme
+            switch cgmProgress?.progressState {
+            case .critical: theme = .red
+            case .warning: theme = .orange
+            default: theme = .green
+            }
+            return (text, theme, nil)
+        }
+        return nil
+    }
+
+    private func theme(for status: CgmDisplayStatus) -> SensorStatusTagTheme {
+        switch status {
+        case .critical: return .red
+        case .warning: return .orange
+        case .normal: return .secondary
+        }
+    }
+
+    private func glucoseColor(for glucoseValue: Int16) -> Color {
+        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+        let hardCodedLow = Decimal(55)
+        let hardCodedHigh = Decimal(220)
+        let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+        guard Decimal(glucoseValue) <= lowGlucose || Decimal(glucoseValue) >= highGlucose else {
+            return Color.primary
+        }
+        return Trio.getDynamicGlucoseColor(
+            glucoseValue: Decimal(glucoseValue),
+            highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+            lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+            targetGlucose: currentGlucoseTarget,
+            glucoseColorScheme: glucoseColorScheme
+        )
+    }
 }
 
 struct Triangle: Shape {
@@ -168,13 +326,16 @@ struct TrendShape: View {
 
     let gradient: AngularGradient
     let color: Color
+    var showArrow: Bool = true
 
     var body: some View {
         HStack(alignment: .center) {
             ZStack {
                 Group {
                     CircleShape(gradient: gradient)
-                    TriangleShape(color: color)
+                    if showArrow {
+                        TriangleShape(color: color)
+                    }
                 }.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.75 : 0.33), radius: colorScheme == .dark ? 5 : 3)
                 CircleShape(gradient: gradient)
             }

+ 22 - 0
Trio/Sources/Modules/Home/View/Header/SensorLifecycle/CGMSensorDisplayState.swift

@@ -0,0 +1,22 @@
+import Foundation
+
+/// Compact `5d 14h` / `22h` / `45m` remaining-time formatter. Used as a
+/// fallback tag label when the CGM doesn't surface a `cgmStatusHighlight`
+/// but the lifecycle progress lets us derive an expiration date.
+enum SensorRemainingTimeFormatter {
+    static func format(until expiresAt: Date, now: Date = Date()) -> String {
+        let remaining = max(0, expiresAt.timeIntervalSince(now))
+        let totalMinutes = Int(remaining) / 60
+        let days = totalMinutes / (60 * 24)
+        let hours = (totalMinutes / 60) % 24
+        let minutes = totalMinutes % 60
+        let d = String(localized: "d", comment: "Abbreviation for Days")
+        let h = String(localized: "h", comment: "Abbreviation for Hours")
+        let m = String(localized: "m", comment: "Abbreviation for Minutes")
+        if days > 0, hours > 0 { return "\(days)\(d) \(hours)\(h)" }
+        if days > 0 { return "\(days)\(d)" }
+        if hours > 0 { return "\(hours)\(h)" }
+        if minutes > 0 { return "\(minutes)\(m)" }
+        return "<" + "\u{00A0}" + "1" + "\u{00A0}" + m
+    }
+}

+ 55 - 0
Trio/Sources/Modules/Home/View/Header/SensorLifecycle/SensorLifecycleArcView.swift

@@ -0,0 +1,55 @@
+import LoopKit
+import LoopKitUI
+import SwiftUI
+
+/// Concentric outer ring around the glucose bobble that drains clockwise from
+/// 12 o'clock as the sensor session ages. Always renders a faint track behind
+/// the fill so the indicator's footprint is visible even at `progress == 0`.
+struct SensorLifecycleArcView: View {
+    let progress: Double
+    let progressState: DeviceLifecycleProgressState
+
+    /// Outer diameter — bobble is ~130pt; arc sits 10pt outside the ring stroke.
+    static let diameter: CGFloat = 146
+    static let strokeWidth: CGFloat = 4
+
+    private var arcColor: Color {
+        switch progressState {
+        case .critical:
+            return Color.loopRed
+        case .warning:
+            return Color.orange
+        case .dimmed,
+             .normalCGM,
+             .normalPump:
+            return Color.loopGreen
+        }
+    }
+
+    var body: some View {
+        ZStack {
+            Circle()
+                .stroke(Color.secondary.opacity(0.4), lineWidth: Self.strokeWidth)
+
+            Circle()
+                .trim(from: 0, to: max(0, min(1, progress)))
+                .stroke(arcColor, style: StrokeStyle(lineWidth: Self.strokeWidth, lineCap: .round))
+                .rotationEffect(.degrees(-90))
+                .animation(.easeInOut(duration: 0.25), value: progress)
+        }
+        .frame(width: Self.diameter, height: Self.diameter)
+        .allowsHitTesting(false)
+    }
+}
+
+#Preview("Arc — progress states") {
+    VStack(spacing: 24) {
+        SensorLifecycleArcView(progress: 0.15, progressState: .normalCGM)
+        SensorLifecycleArcView(progress: 0.55, progressState: .normalCGM)
+        SensorLifecycleArcView(progress: 0.92, progressState: .warning)
+        SensorLifecycleArcView(progress: 1.0, progressState: .critical)
+    }
+    .padding(40)
+    .background(Color.black)
+    .preferredColorScheme(.dark)
+}

+ 56 - 0
Trio/Sources/Modules/Home/View/Header/SensorLifecycle/SensorStatusTagView.swift

@@ -0,0 +1,56 @@
+import SwiftUI
+
+/// Status badge below the glucose bobble — text-only, no fill or border.
+struct SensorStatusTagView: View {
+    let text: String
+    let theme: SensorStatusTagTheme
+    var iconSystemName: String?
+
+    var body: some View {
+        HStack(spacing: 4) {
+            if let iconSystemName {
+                Image(systemName: iconSystemName)
+                    .font(.callout)
+                    .fontWeight(.bold)
+                    .foregroundStyle(theme.textColor)
+            }
+            Text(text)
+                .font(.callout)
+                .fontWeight(.bold)
+                .fontDesign(.rounded)
+                .foregroundStyle(theme.textColor)
+                .lineLimit(1)
+        }
+        .padding(.bottom, -16)
+    }
+}
+
+enum SensorStatusTagTheme {
+    case green
+    case orange
+    case red
+    case secondary
+
+    var textColor: Color {
+        switch self {
+        case .green: return Color.loopGreen
+        case .orange: return Color.orange
+        case .red: return Color.loopRed
+        case .secondary: return Color.secondary
+        }
+    }
+}
+
+#Preview("Tag — all themes") {
+    VStack(spacing: 12) {
+        SensorStatusTagView(text: "5d 14h", theme: .green)
+        SensorStatusTagView(text: "22h left", theme: .orange)
+        SensorStatusTagView(text: "warming up", theme: .secondary)
+        SensorStatusTagView(text: "calibrate", theme: .secondary)
+        SensorStatusTagView(text: "sensor expired", theme: .red)
+        SensorStatusTagView(text: "sensor error", theme: .red)
+    }
+    .padding(40)
+    .background(Color.black)
+    .preferredColorScheme(.dark)
+}

+ 6 - 2
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -133,8 +133,12 @@ extension Home {
                 cgmAvailable: state.cgmAvailable,
                 currentGlucoseTarget: state.currentGlucoseTarget,
                 glucoseColorScheme: state.glucoseColorScheme,
-                glucose: state.latestTwoGlucoseValues
-            ).scaleEffect(0.9)
+                glucose: state.latestTwoGlucoseValues,
+                cgmProgress: state.cgmProgressHighlight,
+                cgmStatus: state.cgmDisplayState,
+                cgmSensorExpiresAt: state.cgmSensorExpiresAt,
+                cgmWarmupEndsAt: state.cgmWarmupEndsAt
+            )
                 .onTapGesture {
                     if !state.cgmAvailable {
                         showCGMSelection.toggle()

+ 4 - 0
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -1,6 +1,7 @@
 import Combine
 import CoreData
 import Foundation
+import LoopKit
 import LoopKitUI
 import Swinject
 import UIKit
@@ -265,6 +266,9 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     var glucoseManager: FetchGlucoseManager?
     var cgmManager: CGMManagerUI?
 
+    let cgmDisplayState = CurrentValueSubject<CgmDisplayState?, Never>(nil)
+    let cgmProgressHighlight = CurrentValueSubject<DeviceLifecycleProgress?, Never>(nil)
+
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
         Future { promise in
             Task {