瀏覽代碼

Merge branch 'dev' into feat/dev-eversense

Deniz Cengiz 12 小時之前
父節點
當前提交
59cffcfe07

+ 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
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.8.1
 APP_VERSION = 0.8.1
-APP_DEV_VERSION = 0.8.1.3
+APP_DEV_VERSION = 0.8.1.5
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 
 

+ 20 - 0
LiveActivity/Assets.xcassets/LoopGreen.colorset/Contents.json

@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.592",
+          "green" : "0.812",
+          "red" : "0.435"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 5 - 0
LiveActivity/LiveActivity.swift

@@ -121,6 +121,11 @@ private extension LiveActivityAttributes.ContentState {
             overrideDate: Date().addingTimeInterval(-3600),
             overrideDate: Date().addingTimeInterval(-3600),
             overrideDuration: 120,
             overrideDuration: 120,
             overrideTarget: 150,
             overrideTarget: 150,
+            isTempTargetActive: false,
+            tempTargetName: "Temp Target",
+            tempTargetDate: Date().addingTimeInterval(-1800),
+            tempTargetDuration: 60,
+            tempTargetTarget: 120,
             widgetItems: LiveActivityAttributes.LiveActivityItem.defaultItems
             widgetItems: LiveActivityAttributes.LiveActivityItem.defaultItems
         )
         )
 
 

+ 24 - 2
LiveActivity/Views/LiveActivityChartView.swift

@@ -33,13 +33,13 @@ struct LiveActivityChartView: View {
         let target = isMgdL ? state.target : state.target.asMmolL
         let target = isMgdL ? state.target : state.target.asMmolL
 
 
         let isOverrideActive = additionalState.isOverrideActive == true
         let isOverrideActive = additionalState.isOverrideActive == true
+        let isTempTargetActive = additionalState.isTempTargetActive == true
 
 
         let calendar = Calendar.current
         let calendar = Calendar.current
         let now = Date()
         let now = Date()
 
 
         let startDate = calendar.date(byAdding: .hour, value: isWatchOS ? -3 : -6, to: now) ?? now
         let startDate = calendar.date(byAdding: .hour, value: isWatchOS ? -3 : -6, to: now) ?? now
-        let endDate = isOverrideActive ? (calendar.date(byAdding: .hour, value: 2, to: now) ?? now) :
-            (calendar.date(byAdding: .minute, value: isWatchOS ? 5 : 0, to: now) ?? now)
+        let endDate = calendar.date(byAdding: .minute, value: isWatchOS ? 5 : 0, to: now) ?? now
 
 
         // 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
         // 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 = isMgdL ? Decimal(55) : 55.asMmolL
         let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
@@ -79,6 +79,10 @@ struct LiveActivityChartView: View {
                 drawActiveOverrides()
                 drawActiveOverrides()
             }
             }
 
 
+            if isTempTargetActive {
+                drawActiveTempTarget()
+            }
+
             drawChart(yAxisRuleMarkMin: yAxisRuleMarkMin, yAxisRuleMarkMax: yAxisRuleMarkMax)
             drawChart(yAxisRuleMarkMin: yAxisRuleMarkMin, yAxisRuleMarkMax: yAxisRuleMarkMax)
         }
         }
         .chartYAxis {
         .chartYAxis {
@@ -125,6 +129,24 @@ struct LiveActivityChartView: View {
         .lineStyle(.init(lineWidth: 8))
         .lineStyle(.init(lineWidth: 8))
     }
     }
 
 
+    private func drawActiveTempTarget() -> some ChartContent {
+        let start: Date = context.state.detailedViewState.tempTargetDate
+
+        let duration = context.state.detailedViewState.tempTargetDuration
+        let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
+
+        let end: Date = start.addingTimeInterval(durationAsTimeInterval)
+        let target = context.state.detailedViewState.tempTargetTarget
+
+        return RuleMark(
+            xStart: .value("Start", start, unit: .second),
+            xEnd: .value("End", end, unit: .second),
+            y: .value("Value", target)
+        )
+        .foregroundStyle(Color("LoopGreen").opacity(0.6))
+        .lineStyle(.init(lineWidth: 8))
+    }
+
     private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
     private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
         // 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
         // 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 hardCodedLow = Decimal(55)

+ 19 - 8
LiveActivity/Views/LiveActivityView.swift

@@ -62,18 +62,29 @@ struct LiveActivityView: View {
                 LiveActivityChartView(context: context, additionalState: context.state.detailedViewState)
                 LiveActivityChartView(context: context, additionalState: context.state.detailedViewState)
                     .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
                     .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
                     .frame(height: 80)
                     .frame(height: 80)
-                    .overlay(alignment: .topTrailing) {
-                        if context.state.detailedViewState.isOverrideActive {
-                            HStack {
-                                Text("\(context.state.detailedViewState.overrideName)")
+                    .overlay(alignment: .topLeading) {
+                        HStack(spacing: 4) {
+                            if context.state.detailedViewState.isOverrideActive {
+                                Text(context.state.detailedViewState.overrideName)
                                     .font(.footnote)
                                     .font(.footnote)
                                     .fontWeight(.bold)
                                     .fontWeight(.bold)
                                     .foregroundStyle(.white)
                                     .foregroundStyle(.white)
+                                    .padding(6)
+                                    .background {
+                                        RoundedRectangle(cornerRadius: 10)
+                                            .fill(Color.purple.opacity(colorScheme == .dark ? 0.6 : 0.8))
+                                    }
                             }
                             }
-                            .padding(6)
-                            .background {
-                                RoundedRectangle(cornerRadius: 10)
-                                    .fill(Color.purple.opacity(colorScheme == .dark ? 0.6 : 0.8))
+                            if context.state.detailedViewState.isTempTargetActive {
+                                Text(context.state.detailedViewState.tempTargetName)
+                                    .font(.footnote)
+                                    .fontWeight(.bold)
+                                    .foregroundStyle(.white)
+                                    .padding(6)
+                                    .background {
+                                        RoundedRectangle(cornerRadius: 10)
+                                            .fill(Color("LoopGreen").opacity(colorScheme == .dark ? 0.6 : 0.8))
+                                    }
                             }
                             }
                         }
                         }
                     }
                     }

+ 41 - 55
Trio Watch App Extension/WatchState.swift

@@ -62,6 +62,11 @@ import WatchConnectivity
 
 
     var recommendedBolus: Decimal = 0
     var recommendedBolus: Decimal = 0
 
 
+    /// Snapshots older than this are dropped at the top of the WC delegate
+    /// methods. Single source of truth for both `didReceiveMessage` and
+    /// `didReceiveUserInfo`.
+    private static let maxAcceptableMessageAgeInMinutes: TimeInterval = 15 * 60
+
     // MARK: - Debouncing and batch processing helpers
     // MARK: - Debouncing and batch processing helpers
 
 
     /// Temporary storage for new data arriving via WatchConnectivity.
     /// Temporary storage for new data arriving via WatchConnectivity.
@@ -180,92 +185,73 @@ import WatchConnectivity
             await WatchLogger.shared.log("⌚️ Watch received data: \(message)")
             await WatchLogger.shared.log("⌚️ Watch received data: \(message)")
         }
         }
 
 
-        // If the message has a nested "watchState" dictionary with date as TimeInterval
-        if let watchStateDict = message[WatchMessageKeys.watchState] as? [String: Any],
-           let timestamp = watchStateDict[WatchMessageKeys.date] as? TimeInterval
-        {
-            let date = Date(timeIntervalSince1970: timestamp)
-
-            // Check if it's not older than 15 min
-            if date >= Date().addingTimeInterval(-15 * 60) {
-                Task {
-                    await WatchLogger.shared.log("⌚️ Handling watchState from \(date)")
-                }
-                processWatchMessage(message)
-            } else {
-                Task {
-                    await WatchLogger.shared.log("⌚️ Received outdated watchState data (\(date))")
-                }
-                DispatchQueue.main.async {
-                    self.showSyncingAnimation = false
-                }
-            }
-            return
-        }
-
-        // Else if the message is an "ack" at the top level
-        // e.g. { "acknowledged": true, "message": "Started Temp Target...", "date": Date(...) }
-        else if
-            let acknowledged = message[WatchMessageKeys.acknowledged] as? Bool,
-            let ackMessage = message[WatchMessageKeys.message] as? String,
-            let ackCodeRaw = message[WatchMessageKeys.ackCode] as? String
+        // Ack at top level — no `watchState` wrapper, no staleness check.
+        if let acknowledged = message[WatchMessageKeys.acknowledged] as? Bool,
+           let ackMessage = message[WatchMessageKeys.message] as? String,
+           let ackCodeRaw = message[WatchMessageKeys.ackCode] as? String
         {
         {
             Task {
             Task {
                 await WatchLogger.shared
                 await WatchLogger.shared
                     .log("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged), ackCode: \(ackCodeRaw)")
                     .log("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged), ackCode: \(ackCodeRaw)")
             }
             }
             DispatchQueue.main.async {
             DispatchQueue.main.async {
-                // For ack messages, we do NOT show “Syncing...”
                 self.showSyncingAnimation = false
                 self.showSyncingAnimation = false
             }
             }
             processWatchMessage(message)
             processWatchMessage(message)
             return
             return
+        }
 
 
-                    // Recommended bolus is also not part of the WatchState message, hence the extra condition here
-        } else if
-            let recommendedBolus = message[WatchMessageKeys.recommendedBolus] as? NSNumber
-        {
+        // Recommended bolus is also not part of the WatchState message.
+        if let recommendedBolus = message[WatchMessageKeys.recommendedBolus] as? NSNumber {
             Task {
             Task {
                 await WatchLogger.shared.log("⌚️ Received recommended bolus: \(recommendedBolus)")
                 await WatchLogger.shared.log("⌚️ Received recommended bolus: \(recommendedBolus)")
             }
             }
-
             DispatchQueue.main.async {
             DispatchQueue.main.async {
                 self.recommendedBolus = recommendedBolus.decimalValue
                 self.recommendedBolus = recommendedBolus.decimalValue
                 self.showBolusCalculationProgress = false
                 self.showBolusCalculationProgress = false
             }
             }
-
             return
             return
-        } else {
-            Task {
-                await WatchLogger.shared.log("⌚️ Faulty data. Skipping...")
-            }
-            DispatchQueue.main.async {
-                self.showSyncingAnimation = false
-            }
         }
         }
+
+        handleIncomingWatchStatePayload(message)
     }
     }
 
 
     func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
     func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
-        guard let snapshot = WatchStateSnapshot(from: userInfo) else {
-            Task {
-                await WatchLogger.shared.log("⌚️ Invalid snapshot received", force: true)
-            }
+        handleIncomingWatchStatePayload(userInfo)
+    }
+
+    /// Shared path for watch-state payloads from either delegate method.
+    /// Enforces the freshness contract in one place so the two delivery paths
+    /// can't drift.
+    private func handleIncomingWatchStatePayload(_ dictionary: [String: Any]) {
+        guard let payload = dictionary[WatchMessageKeys.watchState] as? [String: Any],
+              let timestamp = payload[WatchMessageKeys.date] as? TimeInterval
+        else {
+            Task { await WatchLogger.shared.log("⌚️ Faulty watch state payload — skipping", force: true) }
+            DispatchQueue.main.async { self.showSyncingAnimation = false }
             return
             return
         }
         }
+        let date = Date(timeIntervalSince1970: timestamp)
 
 
-        let lastProcessed = WatchStateSnapshot.loadLatestDateFromDisk()
-
-        guard snapshot.date > lastProcessed else {
-            Task {
-                await WatchLogger.shared.log("⌚️ Ignoring outdated or duplicate WatchState snapshot", force: true)
-            }
+        // Wall-clock staleness gate. Drops the queued backlog cheaply when
+        // the watch app wakes after long disuse; without it, every payload
+        // schedules merge + UI work.
+        guard date >= Date().addingTimeInterval(-Self.maxAcceptableMessageAgeInMinutes) else {
+            Task { await WatchLogger.shared.log("⌚️ Skipping stale watch state (\(date))") }
+            DispatchQueue.main.async { self.showSyncingAnimation = false }
             return
             return
         }
         }
 
 
-        WatchStateSnapshot.saveLatestDateToDisk(snapshot.date)
+        // Monotonicity dedup.
+        let lastProcessed = WatchStateSnapshot.loadLatestDateFromDisk()
+        guard date > lastProcessed else {
+            Task { await WatchLogger.shared.log("⌚️ Skipping duplicate watch state (\(date))") }
+            return
+        }
 
 
+        WatchStateSnapshot.saveLatestDateToDisk(date)
         DispatchQueue.main.async {
         DispatchQueue.main.async {
-            self.scheduleUIUpdate(with: snapshot.payload)
+            self.scheduleUIUpdate(with: payload)
         }
         }
     }
     }
 
 

+ 4 - 23
Trio Watch App Extension/WatchStateSnapshot.swift

@@ -6,34 +6,15 @@
 //
 //
 import Foundation
 import Foundation
 
 
-struct WatchStateSnapshot {
-    let date: Date
-    let payload: [String: Any]
-
-    init?(from dictionary: [String: Any]) {
-        guard let timestamp = dictionary[WatchMessageKeys.date] as? TimeInterval,
-              let payload = dictionary[WatchMessageKeys.watchState] as? [String: Any]
-        else {
-            return nil
-        }
-
-        date = Date(timeIntervalSince1970: timestamp)
-        self.payload = payload
-    }
-
-    func toDictionary() -> [String: Any] {
-        [
-            WatchMessageKeys.date: date.timeIntervalSince1970,
-            WatchMessageKeys.watchState: payload
-        ]
-    }
+enum WatchStateSnapshot {
+    private static let storageKey = "WatchStateSnapshot.latest"
 
 
     static func saveLatestDateToDisk(_ date: Date) {
     static func saveLatestDateToDisk(_ date: Date) {
-        UserDefaults.standard.set(date.timeIntervalSince1970, forKey: "WatchStateSnapshot.latest")
+        UserDefaults.standard.set(date.timeIntervalSince1970, forKey: storageKey)
     }
     }
 
 
     static func loadLatestDateFromDisk() -> Date {
     static func loadLatestDateFromDisk() -> Date {
-        let interval = UserDefaults.standard.double(forKey: "WatchStateSnapshot.latest")
+        let interval = UserDefaults.standard.double(forKey: storageKey)
         return Date(timeIntervalSince1970: interval)
         return Date(timeIntervalSince1970: interval)
     }
     }
 }
 }

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -439,6 +439,7 @@
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.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 */; };
 		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 */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
@@ -1305,6 +1306,7 @@
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
@@ -3211,6 +3213,7 @@
 				BDF34F842C10C62E00D51995 /* GlucoseData.swift */,
 				BDF34F842C10C62E00D51995 /* GlucoseData.swift */,
 				BDF34F942C10D27300D51995 /* DeterminationData.swift */,
 				BDF34F942C10D27300D51995 /* DeterminationData.swift */,
 				BDBAACF92C2D439700370AAE /* OverrideData.swift */,
 				BDBAACF92C2D439700370AAE /* OverrideData.swift */,
+				4B8F3B0E159844FD8746DFC0 /* TempTargetData.swift */,
 			);
 			);
 			path = Data;
 			path = Data;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -4827,6 +4830,7 @@
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
+				BB5227A51D9D4377A1A70BA6 /* TempTargetData.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,

+ 4 - 23
Trio/Sources/Models/WatchStateSnapshot.swift

@@ -6,34 +6,15 @@
 //
 //
 import Foundation
 import Foundation
 
 
-struct WatchStateSnapshot {
-    let date: Date
-    let payload: [String: Any]
-
-    init?(from dictionary: [String: Any]) {
-        guard let timestamp = dictionary[WatchMessageKeys.date] as? TimeInterval,
-              let payload = dictionary[WatchMessageKeys.watchState] as? [String: Any]
-        else {
-            return nil
-        }
-
-        date = Date(timeIntervalSince1970: timestamp)
-        self.payload = payload
-    }
-
-    func toDictionary() -> [String: Any] {
-        [
-            WatchMessageKeys.date: date.timeIntervalSince1970,
-            WatchMessageKeys.watchState: payload
-        ]
-    }
+enum WatchStateSnapshot {
+    private static let storageKey = "WatchStateSnapshot.latest"
 
 
     static func saveLatestDateToDisk(_ date: Date) {
     static func saveLatestDateToDisk(_ date: Date) {
-        UserDefaults.standard.set(date.timeIntervalSince1970, forKey: "WatchStateSnapshot.latest")
+        UserDefaults.standard.set(date.timeIntervalSince1970, forKey: storageKey)
     }
     }
 
 
     static func loadLatestDateFromDisk() -> Date {
     static func loadLatestDateFromDisk() -> Date {
-        let interval = UserDefaults.standard.double(forKey: "WatchStateSnapshot.latest")
+        let interval = UserDefaults.standard.double(forKey: storageKey)
         return Date(timeIntervalSince1970: interval)
         return Date(timeIntervalSince1970: interval)
     }
     }
 }
 }

+ 47 - 14
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 import Foundation
 
 
 // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
 // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
@@ -67,31 +68,63 @@ extension LiveActivityManager {
         }
         }
     }
     }
 
 
+    func fetchAndMapTempTarget() async throws -> TempTargetData? {
+        try await fetchAndMapLatest(
+            ofType: TempTargetStored.self,
+            predicate: .predicateForOneDayAgo,
+            key: "date",
+            propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
+        ) { row in
+            TempTargetData(
+                isActive: row["enabled"] as? Bool ?? false,
+                tempTargetName: row["name"] as? String ?? "Temp Target",
+                date: row["date"] as? Date ?? Date(),
+                duration: row["duration"] as? Decimal ?? 0,
+                target: row["target"] as? Decimal ?? 0
+            )
+        }
+    }
+
     func fetchAndMapOverride() async throws -> OverrideData? {
     func fetchAndMapOverride() async throws -> OverrideData? {
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+        try await fetchAndMapLatest(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
-            onContext: context,
-            predicate: NSPredicate.predicateForOneDayAgo,
+            predicate: .predicateForOneDayAgo,
             key: "date",
             key: "date",
+            propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
+        ) { row in
+            OverrideData(
+                isActive: row["enabled"] as? Bool ?? false,
+                overrideName: row["name"] as? String ?? "Override",
+                date: row["date"] as? Date ?? Date(),
+                duration: row["duration"] as? Decimal ?? 0,
+                target: row["target"] as? Decimal ?? 0
+            )
+        }
+    }
+
+    private func fetchAndMapLatest<Entity: NSManagedObject, Output>(
+        ofType type: Entity.Type,
+        predicate: NSPredicate,
+        key: String,
+        propertiesToFetch: [String],
+        map: @escaping ([String: Any]) -> Output
+    ) async throws -> Output? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: type,
+            onContext: context,
+            predicate: predicate,
+            key: key,
             ascending: false,
             ascending: false,
             fetchLimit: 1,
             fetchLimit: 1,
-            propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
+            propertiesToFetch: propertiesToFetch
         )
         )
 
 
         return try await context.perform {
         return try await context.perform {
-            guard let overrideResults = results as? [[String: Any]] else {
+            guard let rows = results as? [[String: Any]] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
-            return overrideResults.first.map {
-                OverrideData(
-                    isActive: $0["enabled"] as? Bool ?? false,
-                    overrideName: $0["name"] as? String ?? "Override",
-                    date: $0["date"] as? Date ?? Date(),
-                    duration: $0["duration"] as? Decimal ?? 0,
-                    target: $0["target"] as? Decimal ?? 0
-                )
-            }
+            return rows.first.map(map)
         }
         }
     }
     }
 }
 }

+ 9 - 0
Trio/Sources/Services/LiveActivity/Data/TempTargetData.swift

@@ -0,0 +1,9 @@
+import Foundation
+
+struct TempTargetData {
+    let isActive: Bool
+    let tempTargetName: String
+    let date: Date
+    let duration: Decimal
+    let target: Decimal
+}

+ 5 - 0
Trio/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -43,6 +43,11 @@ struct LiveActivityAttributes: ActivityAttributes {
         let overrideDate: Date
         let overrideDate: Date
         let overrideDuration: Decimal
         let overrideDuration: Decimal
         let overrideTarget: Decimal
         let overrideTarget: Decimal
+        let isTempTargetActive: Bool
+        let tempTargetName: String
+        let tempTargetDate: Date
+        let tempTargetDuration: Decimal
+        let tempTargetTarget: Decimal
         let widgetItems: [LiveActivityItem]
         let widgetItems: [LiveActivityItem]
     }
     }
 
 

+ 6 - 0
Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -69,6 +69,7 @@ extension LiveActivityAttributes.ContentState {
         determination: DeterminationData?,
         determination: DeterminationData?,
         iob: Decimal?,
         iob: Decimal?,
         override: OverrideData?,
         override: OverrideData?,
+        tempTarget: TempTargetData?,
         widgetItems: [LiveActivityAttributes.LiveActivityItem]?
         widgetItems: [LiveActivityAttributes.LiveActivityItem]?
     ) {
     ) {
         let glucose = bg.glucose
         let glucose = bg.glucose
@@ -112,6 +113,11 @@ extension LiveActivityAttributes.ContentState {
             overrideDate: override?.date ?? Date(),
             overrideDate: override?.date ?? Date(),
             overrideDuration: override?.duration ?? 0,
             overrideDuration: override?.duration ?? 0,
             overrideTarget: override?.target ?? 0,
             overrideTarget: override?.target ?? 0,
+            isTempTargetActive: tempTarget?.isActive ?? false,
+            tempTargetName: tempTarget?.tempTargetName ?? "Temp Target",
+            tempTargetDate: tempTarget?.date ?? Date(),
+            tempTargetDuration: tempTarget?.duration ?? 0,
+            tempTargetTarget: tempTarget?.target ?? 0,
             widgetItems: widgetItems ?? [] // set empty array here to silence compiler; this can never be nil
             widgetItems: widgetItems ?? [] // set empty array here to silence compiler; this can never be nil
         )
         )
 
 

+ 22 - 0
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -36,6 +36,8 @@ final class LiveActivityData: ObservableObject {
     @Published var glucoseFromPersistence: [GlucoseData]?
     @Published var glucoseFromPersistence: [GlucoseData]?
     /// The current override data (if any).
     /// The current override data (if any).
     @Published var override: OverrideData?
     @Published var override: OverrideData?
+    /// The current temp target data (if any).
+    @Published var tempTarget: TempTargetData?
     /// The widget items displayed within the live activity.
     /// The widget items displayed within the live activity.
     @Published var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
     @Published var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
 }
 }
@@ -141,6 +143,10 @@ final class LiveActivityData: ObservableObject {
             Task { await self?.loadOverrides() }
             Task { await self?.loadOverrides() }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
+        coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
+            Task { await self?.loadTempTarget() }
+        }.store(in: &subscriptions)
+
         coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
         coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             Task { await self?.loadGlucose() }
             Task { await self?.loadGlucose() }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
@@ -179,6 +185,15 @@ final class LiveActivityData: ObservableObject {
         }
         }
     }
     }
 
 
+    /// Fetches and maps temp target data and updates the live activity content state.
+    private func loadTempTarget() async {
+        do {
+            data.tempTarget = try await fetchAndMapTempTarget()
+        } catch {
+            debug(.default, "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map temp target: \(error)")
+        }
+    }
+
     /// Handles changes to the live activity order.
     /// Handles changes to the live activity order.
     ///
     ///
     /// Loads widget items from user defaults and triggers an update to the live activity order.
     /// Loads widget items from user defaults and triggers an update to the live activity order.
@@ -203,6 +218,7 @@ final class LiveActivityData: ObservableObject {
         Task {
         Task {
             await self.loadGlucose()
             await self.loadGlucose()
             await self.loadOverrides()
             await self.loadOverrides()
+            await self.loadTempTarget()
             await self.loadDetermination()
             await self.loadDetermination()
             self.loadWidgetItems()
             self.loadWidgetItems()
         }
         }
@@ -301,6 +317,11 @@ final class LiveActivityData: ObservableObject {
                                 overrideDate: Date.now,
                                 overrideDate: Date.now,
                                 overrideDuration: 0,
                                 overrideDuration: 0,
                                 overrideTarget: 0,
                                 overrideTarget: 0,
+                                isTempTargetActive: false,
+                                tempTargetName: "",
+                                tempTargetDate: Date.now,
+                                tempTargetDuration: 0,
+                                tempTargetTarget: 0,
                                 widgetItems: []
                                 widgetItems: []
                             ),
                             ),
                             isInitialState: true
                             isInitialState: true
@@ -399,6 +420,7 @@ final class LiveActivityData: ObservableObject {
             determination: determination,
             determination: determination,
             iob: data.iob,
             iob: data.iob,
             override: data.override,
             override: data.override,
+            tempTarget: data.tempTarget,
             widgetItems: data.widgetItems
             widgetItems: data.widgetItems
         )
         )
 
 

+ 7 - 8
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -495,12 +495,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             return
             return
         }
         }
 
 
-        // Skip if we already sent this state or older
-        let lastSent = WatchStateSnapshot.loadLatestDateFromDisk()
-        guard lastSent < state.date else {
-            debug(.watchManager, "🕐 Skipping push — newer or equal state already sent")
-            return
-        }
+        // Stamp the snapshot with send time. Each push gets a strictly newer
+        // `date` than the previous one, which is what the watch's monotonicity
+        // dedup relies on — including watch-requested re-pushes when no CGM
+        // tick has bumped the build-time date.
+        var state = state
+        state.date = Date()
 
 
         let message: [String: Any] = watchStateToDictionary(from: state)
         let message: [String: Any] = watchStateToDictionary(from: state)
 
 
@@ -510,12 +510,11 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             session.sendMessage([WatchMessageKeys.watchState: message], replyHandler: nil) { error in
             session.sendMessage([WatchMessageKeys.watchState: message], replyHandler: nil) { error in
                 debug(.watchManager, "❌ Error sending watch state: \(error)")
                 debug(.watchManager, "❌ Error sending watch state: \(error)")
             }
             }
-            WatchStateSnapshot.saveLatestDateToDisk(state.date)
         } else {
         } else {
-            WatchStateSnapshot.saveLatestDateToDisk(state.date)
             session.transferUserInfo([WatchMessageKeys.watchState: message])
             session.transferUserInfo([WatchMessageKeys.watchState: message])
             debug(.watchManager, "📤 Transferred new WatchState snapshot via userInfo")
             debug(.watchManager, "📤 Transferred new WatchState snapshot via userInfo")
         }
         }
+        WatchStateSnapshot.saveLatestDateToDisk(state.date)
     }
     }
 
 
     func sendAcknowledgment(toWatch success: Bool, message: String = "", ackCode: AcknowledgmentCode) {
     func sendAcknowledgment(toWatch success: Bool, message: String = "", ackCode: AcknowledgmentCode) {