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

Merge pull request #470 from nightscout/watch-adjustments

Watch App Adjustments
Sam King 1 год назад
Родитель
Сommit
99851819a6

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit f8708218031a377f76d9cf2b1f1fa711d98537e0
+Subproject commit 200937c3c985de4cb05604cf1d7af2a307dfcaf3

+ 13 - 0
Trio Watch App Extension/Helper/Helper+Enums.swift

@@ -19,6 +19,19 @@ enum AcknowledgementStatus: String, CaseIterable {
     case pending
 }
 
+enum AcknowledgmentCode: String, Codable {
+    case savingCarbs = "saving_carbs"
+    case enactingBolus = "enacting_bolus"
+    case comboComplete = "combo_complete"
+    case carbsLogged = "carbs_logged"
+    case overrideStarted = "override_started"
+    case overrideStopped = "override_stopped"
+    case tempTargetStarted = "temp_target_started"
+    case tempTargetStopped = "temp_target_stopped"
+    case genericSuccess = "success"
+    case genericFailure = "failure"
+}
+
 enum WatchSize {
     case watch40mm
     case watch41mm

+ 9 - 0
Trio Watch App Extension/TrioWatchApp.swift

@@ -1,9 +1,18 @@
 import SwiftUI
 
 @main struct TrioWatchApp: App {
+    @Environment(\.scenePhase) private var scenePhase
+
     var body: some Scene {
         WindowGroup {
             TrioMainWatchView()
         }
+        .onChange(of: scenePhase) { _, newScenePhase in
+            if newScenePhase == .background {
+                Task {
+                    await WatchLogger.shared.flushPersistedLogs()
+                }
+            }
+        }
     }
 }

+ 3 - 3
Trio Watch App Extension/Views/AcknowledgementPendingView.swift

@@ -28,13 +28,13 @@ struct AcknowledgementPendingView: View {
                 if state.isMealBolusCombo {
                     ProgressView()
                     Text(state.mealBolusStep.rawValue).multilineTextAlignment(.center)
-                } else if state.showCommsAnimation {
-                    ProgressView()
-                    Text("Processing…")
                 } else if state.showAcknowledgmentBanner {
                     statusIcon.padding()
                     Text(state.acknowledgmentMessage).multilineTextAlignment(.center)
                         .foregroundStyle(state.acknowledgementStatus == .failure ? Color.loopRed : Color.primary)
+                } else if state.showCommsAnimation {
+                    ProgressView()
+                    Text("Processing…")
                 }
             }
             .padding()

+ 0 - 10
Trio Watch App Extension/Views/BolusConfirmationView.swift

@@ -84,7 +84,6 @@ struct BolusConfirmationView: View {
                         state.sendCarbsRequest(state.carbsAmount, Date())
                         state.carbsAmount = 0 // reset carbs in state
                     }
-                    state.activeBolusAmount = bolusAmount
                     state.sendBolusRequest(Decimal(bolusAmount))
                     bolusAmount = 0 // reset bolus in state
                     confirmationProgress = 0 // reset auth progress
@@ -110,14 +109,5 @@ struct BolusConfirmationView: View {
                 )
             }
         }
-        .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
-        .overlay {
-            if state.showBolusProgressOverlay {
-                BolusProgressOverlay(state: state) {
-                    state.shouldNavigateToRoot = false
-                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
-                }.transition(.opacity)
-            }
-        }
     }
 }

+ 0 - 9
Trio Watch App Extension/Views/BolusInputView.swift

@@ -142,15 +142,6 @@ struct BolusInputView: View {
                     .clipShape(Circle())
             }
         }
-        .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
-        .overlay {
-            if state.showBolusProgressOverlay {
-                BolusProgressOverlay(state: state) {
-                    state.shouldNavigateToRoot = false
-                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
-                }.transition(.opacity)
-            }
-        }
         .onAppear {
             // Set initial bolus amount to recommended value
             // Only do this if user has not updated amount previously, e.g., when navigating to next and then back to this view

+ 86 - 86
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -1,86 +1,86 @@
-import SwiftUI
-
-struct BolusProgressOverlay: View {
-    let state: WatchState
-    let onCancelBolus: () -> Void
-
-    private let progressGradient = LinearGradient(
-        colors: [
-            Color(red: 0.7215686275, green: 0.3411764706, blue: 1), // #B857FF
-            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569), // #9F6CFA
-            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765), // #7C8BF3
-            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961), // #57AAEC
-            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902) // #43BBE9
-        ],
-        startPoint: .leading,
-        endPoint: .trailing
-    )
-
-    private var isWatchStateDated: Bool {
-        // If `lastWatchStateUpdate` is nil, treat as "dated"
-        guard let lastUpdateTimestamp = state.lastWatchStateUpdate else {
-            return true
-        }
-        let now = Date().timeIntervalSince1970
-        let secondsSinceUpdate = now - lastUpdateTimestamp
-        // Return true if last update older than 5 min, so 1 loop cycle
-        return secondsSinceUpdate > 5 * 60
-    }
-
-    private var isSessionUnreachable: Bool {
-        guard let session = state.session else {
-            return true // No session at all => unreachable
-        }
-        // Return true if not .activated OR not reachable
-        return session.activationState != .activated
-    }
-
-    var body: some View {
-        VStack(spacing: 10) {
-            VStack {
-                Text("Bolusing")
-                    .font(.footnote)
-                    .foregroundStyle(.secondary)
-                    .padding(.top)
-
-                ProgressView(value: state.bolusProgress, total: 1.0)
-                    .tint(progressGradient)
-
-                Text(String(
-                    format: String(
-                        localized: "%.2f U of %.2f U",
-                        comment: "Format for showing delivered and active bolus amounts, 'x U of y U' on watch"
-                    ),
-                    state.deliveredAmount,
-                    state.activeBolusAmount
-                ))
-                    .font(.footnote)
-                    .foregroundStyle(.secondary)
-
-                Spacer()
-
-                Button(action: {
-                    state.sendCancelBolusRequest()
-                    onCancelBolus()
-                }) {
-                    Text("Cancel Bolus")
-                }
-                .buttonStyle(.bordered)
-                .padding()
-                .disabled(isWatchStateDated || isSessionUnreachable)
-            }
-            .padding()
-            .background(Color.black.opacity(0.9))
-            .cornerRadius(10)
-        }
-        .scenePadding()
-        .onChange(of: state.bolusProgress) { _, newProgress in
-            if newProgress >= 1.0 {
-                state.activeBolusAmount = 0 // Reset only when bolus is complete
-            }
-        }
-        .onDisappear {
-            state.activeBolusAmount = 0 // Triple-check to reset when view disappears
-        }
-    }
-}
+// import SwiftUI
+//
+// struct BolusProgressOverlay: View {
+//    let state: WatchState
+//    let onCancelBolus: () -> Void
+//
+//    private let progressGradient = LinearGradient(
+//        colors: [
+//            Color(red: 0.7215686275, green: 0.3411764706, blue: 1), // #B857FF
+//            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569), // #9F6CFA
+//            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765), // #7C8BF3
+//            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961), // #57AAEC
+//            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902) // #43BBE9
+//        ],
+//        startPoint: .leading,
+//        endPoint: .trailing
+//    )
+//
+//    private var isWatchStateDated: Bool {
+//        // If `lastWatchStateUpdate` is nil, treat as "dated"
+//        guard let lastUpdateTimestamp = state.lastWatchStateUpdate else {
+//            return true
+//        }
+//        let now = Date().timeIntervalSince1970
+//        let secondsSinceUpdate = now - lastUpdateTimestamp
+//        // Return true if last update older than 5 min, so 1 loop cycle
+//        return secondsSinceUpdate > 5 * 60
+//    }
+//
+//    private var isSessionUnreachable: Bool {
+//        guard let session = state.session else {
+//            return true // No session at all => unreachable
+//        }
+//        // Return true if not .activated OR not reachable
+//        return session.activationState != .activated
+//    }
+//
+//    var body: some View {
+//        VStack(spacing: 10) {
+//            VStack {
+//                Text("Bolusing")
+//                    .font(.footnote)
+//                    .foregroundStyle(.secondary)
+//                    .padding(.top)
+//
+////                ProgressView(value: state.bolusProgress, total: 1.0)
+//                    .tint(progressGradient)
+//
+//                Text(String(
+//                    format: String(
+//                        localized: "%.2f U of %.2f U",
+//                        comment: "Format for showing delivered and active bolus amounts, 'x U of y U' on watch"
+//                    ),
+//                    state.deliveredAmount,
+//                    state.activeBolusAmount
+//                ))
+//                    .font(.footnote)
+//                    .foregroundStyle(.secondary)
+//
+//                Spacer()
+//
+//                Button(action: {
+//                    state.sendCancelBolusRequest()
+//                    onCancelBolus()
+//                }) {
+//                    Text("Cancel Bolus")
+//                }
+//                .buttonStyle(.bordered)
+//                .padding()
+//                .disabled(isWatchStateDated || isSessionUnreachable)
+//            }
+//            .padding()
+//            .background(Color.black.opacity(0.9))
+//            .cornerRadius(10)
+//        }
+//        .scenePadding()
+//        .onChange(of: state.bolusProgress) { _, newProgress in
+//            if newProgress >= 1.0 {
+//                state.activeBolusAmount = 0 // Reset only when bolus is complete
+//            }
+//        }
+//        .onDisappear {
+//            state.activeBolusAmount = 0 // Triple-check to reset when view disappears
+//        }
+//    }
+// }

+ 1 - 13
Trio Watch App Extension/Views/TrioMainWatchView.swift

@@ -96,10 +96,7 @@ struct TrioMainWatchView: View {
                 .tag(1)
             }
             .onAppear {
-                // Hard reset variables when main view appears
-                /// Reset `bolusProgress` and `activeBolusAmount` to ensure no stale bolus progressbar is stuck on home view
-                state.bolusProgress = 0
-                state.activeBolusAmount = 0
+                /// Hard reset variables when main view appears
                 /// Reset `bolusAmount` and `recommendedBolus` to ensure no stale / old value is set when user opens bolus input or meal combo the next time.
                 state.bolusAmount = 0
                 state.recommendedBolus = 0
@@ -226,15 +223,6 @@ struct TrioMainWatchView: View {
             }
         }
         .ignoresSafeArea()
-        .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
-        .overlay {
-            if state.showBolusProgressOverlay {
-                BolusProgressOverlay(state: state) {
-                    state.shouldNavigateToRoot = false
-                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
-                }.transition(.opacity)
-            }
-        }
     }
 
     private func updateRotation(for trend: String?) {

+ 150 - 0
Trio Watch App Extension/WatchLogger.swift

@@ -0,0 +1,150 @@
+import Foundation
+import WatchConnectivity
+
+actor WatchLogger {
+    static let shared = WatchLogger()
+
+    private var logs: [String] = []
+    private let maxEntries = 500
+    private let flushInterval: TimeInterval = 3 * 60
+    private let flushSizeThreshold = 100
+    private var lastFlush = Date()
+
+    private let session = WCSession.default
+    private var timerTask: Task<Void, Never>?
+
+    private init() {
+        Task {
+            await startFlushTimer()
+        }
+    }
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
+        return formatter
+    }
+
+    private func startFlushTimer() async {
+        timerTask = Task {
+            while true {
+                try? await Task.sleep(nanoseconds: UInt64(flushInterval * 1_000_000_000))
+                await flushIfNeeded(force: false)
+            }
+        }
+    }
+
+    func log(
+        _ message: String,
+        force: Bool = false,
+        function: String = #function,
+        file: String = #fileID,
+        line: Int = #line
+    ) async {
+        let shortFile = (file as NSString).lastPathComponent
+        let timestamp = dateFormatter.string(from: Date())
+        let entry = "[\(timestamp)] [\(shortFile):\(line)] \(function) → \(message)"
+
+        logs.append(entry)
+        if logs.count > maxEntries {
+            logs.removeFirst(logs.count - maxEntries)
+        }
+
+        print(entry)
+        await flushIfNeeded(force: force)
+    }
+
+    func flushIfNeeded(force: Bool = false) async {
+        let now = Date()
+        let shouldFlush = force || now.timeIntervalSince(lastFlush) >= flushInterval || logs.count >= flushSizeThreshold
+
+        if shouldFlush {
+            await flushToPhone()
+        }
+    }
+
+    private func flushToPhone() async {
+        guard !logs.isEmpty else {
+            return
+        }
+
+        let payload: [String: Any] = ["watchLogs": logs.joined(separator: "\n")]
+
+        if session.activationState != .activated {
+            session.activate()
+        }
+
+        if session.isReachable {
+            session.sendMessage(payload, replyHandler: nil) { _ in
+                Task {
+                    await self.persistLogsLocally()
+                }
+            }
+        } else {
+            await persistLogsLocally()
+        }
+
+        lastFlush = Date()
+        logs.removeAll()
+    }
+
+    func persistLogsLocally() async {
+        let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+            .appendingPathComponent("logs", isDirectory: true)
+
+        try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
+
+        let logFile = logDir.appendingPathComponent("watch_log.txt")
+        let previousLogFile = logDir.appendingPathComponent("watch_log_prev.txt")
+        let startOfDay = Calendar.current.startOfDay(for: Date())
+
+        if let attributes = try? FileManager.default.attributesOfItem(atPath: logFile.path),
+           let creationDate = attributes[.creationDate] as? Date,
+           creationDate < startOfDay
+        {
+            try? FileManager.default.removeItem(at: previousLogFile)
+            try? FileManager.default.moveItem(at: logFile, to: previousLogFile)
+            FileManager.default.createFile(atPath: logFile.path, contents: nil, attributes: [.creationDate: startOfDay])
+        }
+
+        let fullLog = logs.joined(separator: "\n") + "\n"
+        if let data = fullLog.data(using: .utf8) {
+            if let handle = try? FileHandle(forWritingTo: logFile) {
+                try? handle.seekToEnd()
+                handle.write(data)
+                try? handle.close()
+            } else {
+                try? data.write(to: logFile)
+            }
+        }
+    }
+
+    func flushPersistedLogs() async {
+        let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+            .appendingPathComponent("logs", isDirectory: true)
+        let logFile = logDir.appendingPathComponent("watch_log.txt")
+
+        guard let data = try? Data(contentsOf: logFile),
+              let logString = String(data: data, encoding: .utf8),
+              !logString.isEmpty
+        else { return }
+
+        let payload: [String: Any] = ["watchLogs": logString]
+
+        if session.activationState != .activated {
+            session.activate()
+        }
+
+        if session.isReachable {
+            session.sendMessage(payload, replyHandler: nil) { _ in
+                Task {
+                    await self.persistLogsLocally()
+                }
+            }
+            try? FileManager.default.removeItem(at: logFile)
+        } else {
+            _ = session.transferUserInfo(payload)
+            try? FileManager.default.removeItem(at: logFile)
+        }
+    }
+}

+ 143 - 45
Trio Watch App Extension/WatchState+Requests.swift

@@ -8,20 +8,32 @@ extension WatchState {
     /// - Parameters:
     ///   - amount: The insulin amount to be delivered
     func sendBolusRequest(_ amount: Decimal) {
-        guard let session = session, session.isReachable else { return }
-        isBolusCanceled = false // Reset canceled state when starting new bolus
-        activeBolusAmount = Double(truncating: amount as NSNumber) // Set active bolus amount
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Bolus request aborted: session unreachable")
+            }
+            return
+        }
+
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending bolus request: \(amount)U")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.bolus: amount
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("Error sending bolus request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("Error sending bolus request: \(error.localizedDescription)")
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a carbohydrate entry request to the paired iPhone
@@ -29,7 +41,16 @@ extension WatchState {
     ///   - amount: The amount of carbs in grams
     ///   - date: The timestamp for the carb entry (defaults to current time)
     func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
-        guard let session = session, session.isReachable else { return }
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Carbs request aborted: session unreachable")
+            }
+            return
+        }
+
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending carbs request: \(amount)g at \(date)")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.carbs: amount,
@@ -37,104 +58,162 @@ extension WatchState {
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("Error sending carbs request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("Error sending carbs request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a request to cancel the current override preset to the paired iPhone
     func sendCancelOverrideRequest() {
-        guard let session = session, session.isReachable else { return }
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Cancel override request aborted: session unreachable")
+            }
+            return
+        }
+
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending cancel override request")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.cancelOverride: true
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Error sending cancel override request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a request to activate an override preset to the paired iPhone
     /// - Parameter presetName: The name of the override preset to activate
     func sendActivateOverrideRequest(presetName: String) {
-        guard let session = session, session.isReachable else { return }
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Activate override request aborted: session unreachable")
+            }
+            return
+        }
+
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending activate override request for preset: \(presetName)")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.activateOverride: presetName
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("⌚️ Error sending activate override request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Error sending activate override request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a request to cancel the current temporary target to the paired iPhone
     func sendCancelTempTargetRequest() {
-        guard let session = session, session.isReachable else { return }
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Cancel temp target request aborted: session unreachable")
+            }
+            return
+        }
+
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending cancel temp target request")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.cancelTempTarget: true
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a request to activate a temporary target preset to the paired iPhone
     /// - Parameter presetName: The name of the temporary target preset to activate
     func sendActivateTempTargetRequest(presetName: String) {
-        guard let session = session, session.isReachable else { return }
-
-        let message: [String: Any] = [
-            WatchMessageKeys.activateTempTarget: presetName
-        ]
-
-        session.sendMessage(message, replyHandler: nil) { error in
-            print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Activate temp target request aborted: session unreachable")
+            }
+            return
         }
 
-        // Display pending communication animation
-        showCommsAnimation = true
-    }
-
-    /// Sends a request to cancel the current bolus delivery to the paired iPhone
-    func sendCancelBolusRequest() {
-        isBolusCanceled = true
-
-        guard let session = session, session.isReachable else { return }
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending activate temp target request for preset: \(presetName)")
+        }
 
         let message: [String: Any] = [
-            WatchMessageKeys.cancelBolus: true
+            WatchMessageKeys.activateTempTarget: presetName
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("Error sending cancel bolus request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
+            }
         }
 
-        // Reset when cancelled
-        bolusProgress = 0
-        activeBolusAmount = 0
-
         // Display pending communication animation
         showCommsAnimation = true
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a request to calculate a bolus recommendation based on the current carbs amount
     func requestBolusRecommendation() {
-        guard let session = session, session.isReachable else { return }
+        guard let session = session, session.isReachable else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Bolus recommendation request aborted: session unreachable")
+            }
+            return
+        }
+
+        Task {
+            await WatchLogger.shared.log("⌚️ Requesting bolus recommendation for carbs: \(carbsAmount)")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.requestBolusRecommendation: true,
@@ -142,29 +221,48 @@ extension WatchState {
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            print("Error requesting bolus recommendation: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("Error requesting bolus recommendation: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
+            }
         }
-
-        showBolusCalculationProgress = true
     }
 
     func requestWatchStateUpdate() {
-        guard let session = session, session.activationState == .activated else {
-            print("⌚️ Session not activated, activating...")
-            session?.activate()
+        guard let session = session else {
+            Task {
+                await WatchLogger.shared.log("⌚️ No session available for state update")
+            }
+            return
+        }
+
+        guard session.activationState == .activated else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Session not activated. Activating...")
+            }
+            session.activate()
             return
         }
 
         if session.isReachable {
-            print("⌚️ Request an update for watch state from Trio iPhone app...")
+            Task {
+                await WatchLogger.shared.log("⌚️ Requesting WatchState update from iPhone")
+            }
 
             let message = [WatchMessageKeys.requestWatchUpdate: WatchMessageKeys.watchState]
 
             session.sendMessage(message, replyHandler: nil) { error in
-                print("⌚️ Update request for fresh watch state data: \(error.localizedDescription)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Error requesting WatchState update: \(error.localizedDescription)")
+                    await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                    await WatchLogger.shared.persistLogsLocally()
+                }
             }
         } else {
-            print("⌚️ Phone not reachable for watch state update")
+            Task {
+                await WatchLogger.shared.log("⌚️ Phone not reachable for WatchState update")
+            }
         }
     }
 }

+ 140 - 165
Trio Watch App Extension/WatchState.swift

@@ -36,11 +36,6 @@ import WatchConnectivity
     var bolusAmount: Double = 0.0
     var confirmationProgress: Double = 0.0
 
-    var bolusProgress: Double = 0.0
-    var activeBolusAmount: Double = 0.0
-    var deliveredAmount: Double = 0.0
-    var isBolusCanceled = false
-
     // Safety limits
     var maxBolus: Decimal = 10
     var maxCarbs: Decimal = 250
@@ -65,13 +60,9 @@ import WatchConnectivity
     var mealBolusStep: MealBolusStep = .savingCarbs
     var isMealBolusCombo: Bool = false
 
-    var showBolusProgressOverlay: Bool {
-        (!showAcknowledgmentBanner || !showCommsAnimation) && bolusProgress > 0 && bolusProgress < 1.0 && !isBolusCanceled
-    }
-
     var recommendedBolus: Decimal = 0
 
-    // Debouncing and batch processing helpers
+    // MARK: - Debouncing and batch processing helpers
 
     /// Temporary storage for new data arriving via WatchConnectivity.
     private var pendingData: [String: Any] = [:]
@@ -96,32 +87,58 @@ import WatchConnectivity
             session.delegate = self
             session.activate()
             self.session = session
+            Task {
+                await WatchLogger.shared.log("⌚️ WCSession setup complete.")
+            }
         } else {
-            print("⌚️ WCSession is not supported on this device")
+            Task {
+                await WatchLogger.shared.log("⌚️ WCSession is not supported on this device")
+            }
         }
     }
 
     // MARK: – Handle Acknowledgement Messages FROM Phone
 
     func handleAcknowledgment(success: Bool, message: String, isFinal: Bool = true) {
+        Task {
+            await WatchLogger.shared.log("Handling acknowledgment: \(message), success: \(success), isFinal: \(isFinal)")
+        }
+
         if success {
-            print("⌚️ Acknowledgment received: \(message)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Acknowledgment received: \(message)")
+            }
             acknowledgementStatus = .success
-            acknowledgmentMessage = "\(message)"
+            acknowledgmentMessage = message
+
+            // Hide progress animation
+            DispatchQueue.main.async {
+                self.showCommsAnimation = false
+            }
         } else {
-            print("⌚️ Acknowledgment failed: \(message)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Acknowledgment failed: \(message)")
+            }
+
+            // Hide progress animation
             DispatchQueue.main.async {
-                self.showCommsAnimation = false // Hide progress animation
+                self.showCommsAnimation = false
             }
             acknowledgementStatus = .failure
             acknowledgmentMessage = "\(message)"
         }
 
         if isFinal {
-            showAcknowledgmentBanner = true
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+                self.showAcknowledgmentBanner = true
+            }
+
             DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                 self.showAcknowledgmentBanner = false
                 self.showSyncingAnimation = false // Just ensure this is 100% set to false
+                Task {
+                    await WatchLogger.shared.log("Cleared ack banner and syncing animation")
+                }
             }
         }
     }
@@ -133,25 +150,35 @@ import WatchConnectivity
     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
         DispatchQueue.main.async {
             if let error = error {
-                print("⌚️ Watch session activation failed: \(error.localizedDescription)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Watch session activation failed: \(error.localizedDescription)", force: true)
+                    await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                    await WatchLogger.shared.persistLogsLocally()
+                }
                 return
             }
 
             if activationState == .activated {
-                print("⌚️ Watch session activated with state: \(activationState.rawValue)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Watch session activated with state: \(activationState.rawValue)")
+                }
 
                 self.forceConditionalWatchStateUpdate()
 
                 self.isReachable = session.isReachable
 
-                print("⌚️ Watch isReachable after activation: \(session.isReachable)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Watch isReachable after activation: \(session.isReachable)")
+                }
             }
         }
     }
 
     /// Handles incoming messages from the paired iPhone when Phone is in the foreground
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
-        print("⌚️ Watch received data: \(message)")
+        Task {
+            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],
@@ -161,10 +188,14 @@ import WatchConnectivity
 
             // Check if it's not older than 15 min
             if date >= Date().addingTimeInterval(-15 * 60) {
-                print("⌚️ Handling watchState from \(date)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Handling watchState from \(date)")
+                }
                 processWatchMessage(message)
             } else {
-                print("⌚️ Received outdated watchState data (\(date))")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Received outdated watchState data (\(date))")
+                }
                 DispatchQueue.main.async {
                     self.showSyncingAnimation = false
                 }
@@ -176,9 +207,13 @@ import WatchConnectivity
         // 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 ackMessage = message[WatchMessageKeys.message] as? String,
+            let ackCodeRaw = message[WatchMessageKeys.ackCode] as? String
         {
-            print("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged)")
+            Task {
+                await WatchLogger.shared
+                    .log("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged), ackCode: \(ackCodeRaw)")
+            }
             DispatchQueue.main.async {
                 // For ack messages, we do NOT show “Syncing...”
                 self.showSyncingAnimation = false
@@ -190,7 +225,9 @@ import WatchConnectivity
         } else if
             let recommendedBolus = message[WatchMessageKeys.recommendedBolus] as? NSNumber
         {
-            print("⌚️ Received recommended bolus: \(recommendedBolus)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Received recommended bolus: \(recommendedBolus)")
+            }
 
             DispatchQueue.main.async {
                 self.recommendedBolus = recommendedBolus.decimalValue
@@ -198,142 +235,46 @@ import WatchConnectivity
             }
 
             return
-
-                    // Handle bolus progress updates
-        } else if
-            let timestamp = message[WatchMessageKeys.bolusProgressTimestamp] as? TimeInterval,
-            let progress = message[WatchMessageKeys.bolusProgress] as? Double,
-            let activeBolusAmount = message[WatchMessageKeys.activeBolusAmount] as? Double,
-            let deliveredAmount = message[WatchMessageKeys.deliveredAmount] as? Double
-        {
-            let date = Date(timeIntervalSince1970: timestamp)
-
-            // Check if it's not older than 5 min
-            if date >= Date().addingTimeInterval(-5 * 60) {
-                print("⌚️ Handling bolusProgress (sent at \(date))")
-                DispatchQueue.main.async {
-                    if !self.isBolusCanceled {
-                        self.bolusProgress = progress
-                        self.activeBolusAmount = activeBolusAmount
-                        self.deliveredAmount = deliveredAmount
-                    }
-                }
-            } else {
-                print("⌚️ Received outdated bolus progress (sent at \(date))")
-                DispatchQueue.main.async {
-                    self.bolusProgress = 0
-                    self.activeBolusAmount = 0
-                }
-            }
-            return
-
-                    // Handle bolus cancellation
-        } else if
-            message[WatchMessageKeys.bolusCanceled] as? Bool == true
-        {
-            DispatchQueue.main.async {
-                self.bolusProgress = 0
-                self.activeBolusAmount = 0
-                self
-                    .isBolusCanceled =
-                    false /// Reset flag to ensure a bolus progress is also shown after canceling bolus from watch
-            }
-            return
         } else {
-            print("⌚️ Faulty data. Skipping...")
+            Task {
+                await WatchLogger.shared.log("⌚️ Faulty data. Skipping...")
+            }
             DispatchQueue.main.async {
                 self.showSyncingAnimation = false
             }
         }
     }
 
-    /// Handles incoming messages from the paired iPhone when Phone is in the background
     func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
-        print("⌚️ Watch received data: \(userInfo)")
-
-        // If the message has a nested "watchState" dictionary with date as TimeInterval
-        if let watchStateDict = userInfo[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) {
-                print("⌚️ Handling watchState from \(date)")
-                processWatchMessage(userInfo)
-            } else {
-                print("⌚️ Received outdated watchState data (\(date))")
-                DispatchQueue.main.async {
-                    self.showSyncingAnimation = false
-                }
+        guard let snapshot = WatchStateSnapshot(from: userInfo) else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Invalid snapshot received", force: true)
             }
             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 = userInfo[WatchMessageKeys.acknowledged] as? Bool,
-            let ackMessage = userInfo[WatchMessageKeys.message] as? String
-        {
-            print("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged)")
-            DispatchQueue.main.async {
-                // For ack messages, we do NOT show “Syncing...”
-                self.showSyncingAnimation = false
-            }
-            processWatchMessage(userInfo)
-            return
+        let lastProcessed = WatchStateSnapshot.loadLatestDateFromDisk()
 
-                    // Recommended bolus is also not part of the WatchState message, hence the extra condition here
-        } else if
-            let recommendedBolus = userInfo[WatchMessageKeys.recommendedBolus] as? NSNumber
-        {
-            print("⌚️ Received recommended bolus: \(recommendedBolus)")
-            self.recommendedBolus = recommendedBolus.decimalValue
-            showBolusCalculationProgress = false
+        guard snapshot.date > lastProcessed else {
+            Task {
+                await WatchLogger.shared.log("⌚️ Ignoring outdated or duplicate WatchState snapshot", force: true)
+            }
             return
+        }
 
-                    // Handle bolus progress updates
-        } else if
-            let timestamp = userInfo[WatchMessageKeys.bolusProgressTimestamp] as? TimeInterval,
-            let progress = userInfo[WatchMessageKeys.bolusProgress] as? Double,
-            let activeBolusAmount = userInfo[WatchMessageKeys.activeBolusAmount] as? Double,
-            let deliveredAmount = userInfo[WatchMessageKeys.deliveredAmount] as? Double
-        {
-            let date = Date(timeIntervalSince1970: timestamp)
+        WatchStateSnapshot.saveLatestDateToDisk(snapshot.date)
 
-            // Check if it's not older than 5 min
-            if date >= Date().addingTimeInterval(-5 * 60) {
-                print("⌚️ Handling bolusProgress (sent at \(date))")
-                DispatchQueue.main.async {
-                    if !self.isBolusCanceled {
-                        self.bolusProgress = progress
-                        self.activeBolusAmount = activeBolusAmount
-                        self.deliveredAmount = deliveredAmount
-                    }
-                }
-            } else {
-                print("⌚️ Received outdated bolus progress (sent at \(date))")
-                DispatchQueue.main.async {
-                    self.bolusProgress = 0
-                    self.activeBolusAmount = 0
-                }
-            }
-            return
+        DispatchQueue.main.async {
+            self.scheduleUIUpdate(with: snapshot.payload)
+        }
+    }
 
-                    // Handle bolus cancellation
-        } else if
-            userInfo[WatchMessageKeys.bolusCanceled] as? Bool == true
-        {
-            DispatchQueue.main.async {
-                self.bolusProgress = 0
-                self.activeBolusAmount = 0
-            }
-            return
-        } else {
-            print("⌚️ Faulty data. Skipping...")
-            DispatchQueue.main.async {
-                self.showSyncingAnimation = false
+    func session(_: WCSession, didFinish _: WCSessionUserInfoTransfer, error: (any Error)?) {
+        if let error = error {
+            Task {
+                await WatchLogger.shared.log("⌚️ transferUserInfo failed with error: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
+                await WatchLogger.shared.persistLogsLocally()
             }
         }
     }
@@ -342,7 +283,9 @@ import WatchConnectivity
     /// Updates the local reachability status
     func sessionReachabilityDidChange(_ session: WCSession) {
         DispatchQueue.main.async {
-            print("⌚️ Watch reachability changed: \(session.isReachable)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Watch reachability changed: \(session.isReachable)")
+            }
 
             if session.isReachable {
                 self.forceConditionalWatchStateUpdate()
@@ -367,6 +310,10 @@ import WatchConnectivity
     /// it will show a syncing animation and request a new watch state update from the iPhone app.
     private func forceConditionalWatchStateUpdate() {
         guard let lastUpdateTimestamp = lastWatchStateUpdate else {
+            Task {
+                await WatchLogger.shared.log("Forcing initial WatchState update")
+            }
+
             // If there's no recorded timestamp, we must force a fresh update immediately.
             showSyncingAnimation = true
             requestWatchStateUpdate()
@@ -375,6 +322,9 @@ import WatchConnectivity
 
         let now = Date().timeIntervalSince1970
         let secondsSinceUpdate = now - lastUpdateTimestamp
+        Task {
+            await WatchLogger.shared.log("Time since last update: \(secondsSinceUpdate) seconds")
+        }
 
         // If more than 15 seconds have elapsed since the last update, force an(other) update.
         if secondsSinceUpdate > 15 {
@@ -389,26 +339,30 @@ import WatchConnectivity
         DispatchQueue.main.async {
             // 1) Acknowledgment logic
             if let acknowledged = message[WatchMessageKeys.acknowledged] as? Bool,
-               let ackMessage = message[WatchMessageKeys.message] as? String
+               let ackMessage = message[WatchMessageKeys.message] as? String,
+               let ackCodeRaw = message[WatchMessageKeys.ackCode] as? String,
+               let ackCode = AcknowledgmentCode(rawValue: ackCodeRaw)
             {
                 DispatchQueue.main.async {
                     self.showSyncingAnimation = false
                 }
 
-                print("⌚️ Received acknowledgment: \(ackMessage), success: \(acknowledged)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Received acknowledgment: \(ackMessage), success: \(acknowledged)")
+                }
 
-                switch ackMessage {
-                case "Saving carbs...":
+                switch ackCode {
+                case .savingCarbs:
                     self.isMealBolusCombo = true
                     self.mealBolusStep = .savingCarbs
                     self.showCommsAnimation = true
                     self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
-                case "Enacting bolus...":
+                case .enactingBolus:
                     self.isMealBolusCombo = true
                     self.mealBolusStep = .enactingBolus
                     self.showCommsAnimation = true
                     self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
-                case "Carbs and bolus logged successfully":
+                case .comboComplete:
                     self.isMealBolusCombo = false
                     self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
                 default:
@@ -426,11 +380,25 @@ import WatchConnectivity
 
     /// Accumulate new data, set isSyncing, and debounce final update
     private func scheduleUIUpdate(with newData: [String: Any]) {
+        if let incomingTimestamp = newData[WatchMessageKeys.date] as? TimeInterval,
+           let lastTimestamp = lastWatchStateUpdate,
+           incomingTimestamp <= lastTimestamp
+        {
+            Task {
+                await WatchLogger.shared.log("Skipping UI update — outdated WatchState (\(incomingTimestamp))")
+            }
+            return
+        }
+
         // 1) Mark as syncing
         DispatchQueue.main.async {
             self.showSyncingAnimation = true
         }
 
+        Task {
+            await WatchLogger.shared.log("Merging new WatchState data with keys: \(newData.keys.joined(separator: ", "))")
+        }
+
         // 2) Merge data into our pendingData
         pendingData.merge(newData) { _, newVal in newVal }
 
@@ -439,6 +407,9 @@ import WatchConnectivity
 
         // 4) Create and schedule a new finalization
         let workItem = DispatchWorkItem { [self] in
+            Task {
+                await WatchLogger.shared.log("⏳ Debounced update fired")
+            }
             self.finalizePendingData()
         }
         finalizeWorkItem = workItem
@@ -448,6 +419,10 @@ import WatchConnectivity
     /// Applies all pending data to the watch state in one shot
     private func finalizePendingData() {
         guard !pendingData.isEmpty else {
+            Task {
+                await WatchLogger.shared.log("⚠️ finalizePendingData called with empty data")
+            }
+
             // If we have no actual data, just end syncing
             DispatchQueue.main.async {
                 self.showSyncingAnimation = false
@@ -455,7 +430,9 @@ import WatchConnectivity
             return
         }
 
-        print("⌚️ Finalizing pending data: \(pendingData)")
+        Task {
+            await WatchLogger.shared.log("⌚️ Finalizing pending data")
+        }
 
         // Actually set your main UI properties here
         processRawDataForWatchState(pendingData)
@@ -467,12 +444,23 @@ import WatchConnectivity
         DispatchQueue.main.async {
             self.showSyncingAnimation = false
         }
+
+        Task {
+            await WatchLogger.shared.log("✅ Watch UI update complete")
+        }
     }
 
     /// Updates the UI properties
     private func processRawDataForWatchState(_ message: [String: Any]) {
+        Task {
+            await WatchLogger.shared.log("Processing raw WatchState data with keys: \(message.keys.joined(separator: ", "))")
+        }
+
         if let timestamp = message[WatchMessageKeys.date] as? TimeInterval {
             lastWatchStateUpdate = timestamp
+            Task {
+                await WatchLogger.shared.log("Updated lastWatchStateUpdate: \(timestamp)")
+            }
         }
 
         if let currentGlucose = message[WatchMessageKeys.currentGlucose] as? String {
@@ -549,22 +537,9 @@ import WatchConnectivity
             }
         }
 
-        if let bolusProgress = message[WatchMessageKeys.bolusProgress] as? Double {
-            if !isBolusCanceled {
-                self.bolusProgress = bolusProgress
-            }
-        }
-
-        if let bolusWasCanceled = message[WatchMessageKeys.bolusCanceled] as? Bool, bolusWasCanceled {
-            bolusProgress = 0
-            activeBolusAmount = 0
-        }
-
         if let maxBolusValue = message[WatchMessageKeys.maxBolus] {
-            print("⌚️ Received maxBolus: \(maxBolusValue) of type \(type(of: maxBolusValue))")
             if let decimalValue = (maxBolusValue as? NSNumber)?.decimalValue {
                 maxBolus = decimalValue
-                print("⌚️ Converted maxBolus to: \(decimalValue)")
             }
         }
 

+ 39 - 0
Trio Watch App Extension/WatchStateSnapshot.swift

@@ -0,0 +1,39 @@
+//
+//  WatchStateSnapshot.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 18.04.25.
+//
+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
+        ]
+    }
+
+    static func saveLatestDateToDisk(_ date: Date) {
+        UserDefaults.standard.set(date.timeIntervalSince1970, forKey: "WatchStateSnapshot.latest")
+    }
+
+    static func loadLatestDateFromDisk() -> Date {
+        let interval = UserDefaults.standard.double(forKey: "WatchStateSnapshot.latest")
+        return Date(timeIntervalSince1970: interval)
+    }
+}

+ 12 - 0
Trio.xcodeproj/project.pbxproj

@@ -660,6 +660,9 @@
 		DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */; };
 		DDF847E82C5DABA30049BB3B /* WatchConfigAppleWatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E72C5DABA30049BB3B /* WatchConfigAppleWatchView.swift */; };
 		DDF847EA2C5DABAC0049BB3B /* WatchConfigGarminView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E92C5DABAC0049BB3B /* WatchConfigGarminView.swift */; };
+		DDFF204A2DB29EF500AB8A96 /* WatchLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF20492DB29EF500AB8A96 /* WatchLogger.swift */; };
+		DDFF204E2DB2C00B00AB8A96 /* WatchStateSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF204D2DB2C00B00AB8A96 /* WatchStateSnapshot.swift */; };
+		DDFF20502DB2C11900AB8A96 /* WatchStateSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */; };
 		DDFF202F2DB1D14500AB8A96 /* NotificationPermissionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF202E2DB1D14500AB8A96 /* NotificationPermissionStepView.swift */; };
 		DDFF20312DB1D15500AB8A96 /* BluetoothPermissionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF20302DB1D15500AB8A96 /* BluetoothPermissionStepView.swift */; };
 		E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEBFD27368630002FF094 /* ServiceAssembly.swift */; };
@@ -1459,6 +1462,9 @@
 		DDF847E52C5D66490049BB3B /* AddMealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMealPresetView.swift; sourceTree = "<group>"; };
 		DDF847E72C5DABA30049BB3B /* WatchConfigAppleWatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigAppleWatchView.swift; sourceTree = "<group>"; };
 		DDF847E92C5DABAC0049BB3B /* WatchConfigGarminView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigGarminView.swift; sourceTree = "<group>"; };
+		DDFF20492DB29EF500AB8A96 /* WatchLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchLogger.swift; sourceTree = "<group>"; };
+		DDFF204D2DB2C00B00AB8A96 /* WatchStateSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStateSnapshot.swift; sourceTree = "<group>"; };
+		DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStateSnapshot.swift; sourceTree = "<group>"; };
 		DDFF202E2DB1D14500AB8A96 /* NotificationPermissionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionStepView.swift; sourceTree = "<group>"; };
 		DDFF20302DB1D15500AB8A96 /* BluetoothPermissionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothPermissionStepView.swift; sourceTree = "<group>"; };
 		E00EEBFD27368630002FF094 /* ServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = "<group>"; };
@@ -2286,6 +2292,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */,
 				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
 				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
@@ -2925,6 +2932,8 @@
 		BDFF7A9C2D25FA730016C40C /* Trio Watch App Extension */ = {
 			isa = PBXGroup;
 			children = (
+				DDFF204D2DB2C00B00AB8A96 /* WatchStateSnapshot.swift */,
+				DDFF20492DB29EF500AB8A96 /* WatchLogger.swift */,
 				BDA25EE52D260D5800035F34 /* WatchState.swift */,
 				BDAE3FFF2D372BA8009C12B1 /* WatchState+Requests.swift */,
 				DD3A3CEC2D29CFBA00AE478E /* Helper */,
@@ -4063,6 +4072,7 @@
 				388E595C25AD948C0019842D /* TrioApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				DD1745352C55AE7E00211FAC /* TargetBehavoirRootView.swift in Sources */,
+				DDFF20502DB2C11900AB8A96 /* WatchStateSnapshot.swift in Sources */,
 				5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */,
 				38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */,
 				CEE9A6582BBB418300EB5194 /* CalibrationsStateModel.swift in Sources */,
@@ -4589,6 +4599,7 @@
 				BD54A95C2D2808A300F9C1EE /* OverridePresetWatch.swift in Sources */,
 				BD54A9592D27FB7800F9C1EE /* OverridePresetsView.swift in Sources */,
 				BDA25F1E2D26D5DD00035F34 /* GlucoseChartView.swift in Sources */,
+				DDFF204E2DB2C00B00AB8A96 /* WatchStateSnapshot.swift in Sources */,
 				DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */,
 				DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */,
 				DD246F062D2836AA0027DDE0 /* GlucoseTrendView.swift in Sources */,
@@ -4600,6 +4611,7 @@
 				BDA25EE62D260D5E00035F34 /* WatchState.swift in Sources */,
 				BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */,
 				DD09D5C92D29F3D0000D82C9 /* AcknowledgementPendingView.swift in Sources */,
+				DDFF204A2DB29EF500AB8A96 /* WatchLogger.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 1 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "b10fee57248e5d754951672d55dd1e425fadd3089d06858aed6f0f5206be7e5c",
+  "originHash" : "89074a88ed67a58ecd7534519854c5a0928a4046d7c8a6123a7d70f27bf8b44d",
   "pins" : [
     {
       "identity" : "abseil-cpp-binary",

+ 9 - 3
Trio/Sources/APS/APSManager.swift

@@ -514,7 +514,10 @@ final class BaseAPSManager: APSManager, Injectable {
             return
         }
 
-        guard let pump = pumpManager else { return }
+        guard let pump = pumpManager else {
+            callback?(false, String(localized: "Error! Failed to enact bolus.", comment: "Error message for enacting a bolus"))
+            return
+        }
 
         let roundedAmount = pump.roundToSupportedBolusVolume(units: amount)
 
@@ -542,7 +545,7 @@ final class BaseAPSManager: APSManager, Injectable {
             }
             callback?(
                 false,
-                String(localized: "Error! Failed to enact bolus.", comment: "Error message for failing to enact a bolus")
+                String(localized: "Error! Bolus failed with error: \(error.localizedDescription)")
             )
         }
     }
@@ -559,7 +562,10 @@ final class BaseAPSManager: APSManager, Injectable {
             processError(APSError.pumpError(error))
             callback?(
                 false,
-                String(localized: "Error! Bolus cancellation failed.", comment: "Error message for canceling a bolus")
+                String(
+                    localized: "Error! Bolus cancellation failed with error: \(error.localizedDescription)",
+                    comment: "Error message for canceling a bolus"
+                )
             )
         }
         bolusReporter?.removeObserver(self)

+ 2 - 3
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -394,13 +394,12 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         )
 
         return try await context.perform {
-            guard let fetchedResults = results as? [OverrideStored],
-                  let latestOverride = fetchedResults.first
+            guard let fetchedResults = results as? [OverrideStored]
             else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
-            return latestOverride.objectID
+            return fetchedResults.first?.objectID
         }
     }
 }

+ 10 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -6780,6 +6780,7 @@
     },
     "%.2f U of %.2f U" : {
       "comment" : "Format for showing delivered and active bolus amounts, 'x U of y U' on watch",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -52393,6 +52394,7 @@
       }
     },
     "Cancel Bolus" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -94781,8 +94783,12 @@
         }
       }
     },
+    "Error! Bolus cancellation failed with error: %@" : {
+      "comment" : "Error message for canceling a bolus"
+    },
     "Error! Bolus cancellation failed." : {
       "comment" : "Error message for canceling a bolus",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -94882,8 +94888,11 @@
         }
       }
     },
+    "Error! Bolus failed with error: %@" : {
+
+    },
     "Error! Failed to enact bolus." : {
-      "comment" : "Error message for enacting a bolus\nError message for failing to enact a bolus",
+      "comment" : "Error message for enacting a bolus",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

+ 40 - 0
Trio/Sources/Logger/IssueReporter/SimpleLogReporter.swift

@@ -70,6 +70,46 @@ final class SimpleLogReporter: IssueReporter {
     }
 }
 
+extension SimpleLogReporter {
+    static var watchLogFile: String {
+        getDocumentsDirectory().appendingPathComponent("logs/watch_log.txt").path
+    }
+
+    static var watchLogFilePrev: String {
+        getDocumentsDirectory().appendingPathComponent("logs/watch_log_prev.txt").path
+    }
+
+    static func appendToWatchLog(_ logContent: String) {
+        let fileManager = FileManager.default
+        let logDir = getDocumentsDirectory().appendingPathComponent("logs")
+        let logFile = URL(fileURLWithPath: watchLogFile)
+        let prevLogFile = URL(fileURLWithPath: watchLogFilePrev)
+
+        let now = Date()
+        let startOfDay = Calendar.current.startOfDay(for: now)
+
+        // Create logs directory if needed
+        if !fileManager.fileExists(atPath: logDir.path) {
+            try? fileManager.createDirectory(at: logDir, withIntermediateDirectories: true)
+        }
+
+        // Rotate if needed
+        if fileManager.fileExists(atPath: logFile.path),
+           let attributes = try? fileManager.attributesOfItem(atPath: logFile.path),
+           let creationDate = attributes[.creationDate] as? Date,
+           creationDate < startOfDay
+        {
+            try? fileManager.removeItem(at: prevLogFile)
+            try? fileManager.moveItem(at: logFile, to: prevLogFile)
+            fileManager.createFile(atPath: logFile.path, contents: nil, attributes: [.creationDate: startOfDay])
+        }
+
+        if let data = (logContent + "\n").data(using: .utf8) {
+            try? data.append(fileURL: logFile)
+        }
+    }
+}
+
 private extension Data {
     func append(fileURL: URL) throws {
         if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {

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

@@ -1,9 +1,11 @@
 enum WatchMessageKeys {
     // Request/Response Keys
     static let date = "date"
+    static let units = "units"
     static let requestWatchUpdate = "requestWatchUpdate"
     static let watchState = "watchState"
     static let acknowledged = "acknowledged"
+    static let ackCode = "ackCode"
     static let message = "message"
 
     // Treatment Keys

+ 1 - 1
Trio/Sources/Models/WatchState.swift

@@ -1,7 +1,7 @@
 import Foundation
 import SwiftUI
 
-struct WatchState: Hashable, Equatable, Sendable, Encodable {
+struct WatchState: Hashable, Equatable, Sendable, Encodable, Decodable {
     var date: Date
     var currentGlucose: String?
     var currentGlucoseColorString: String?

+ 39 - 0
Trio/Sources/Models/WatchStateSnapshot.swift

@@ -0,0 +1,39 @@
+//
+//  WatchStateSnapshot.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 18.04.25.
+//
+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
+        ]
+    }
+
+    static func saveLatestDateToDisk(_ date: Date) {
+        UserDefaults.standard.set(date.timeIntervalSince1970, forKey: "WatchStateSnapshot.latest")
+    }
+
+    static func loadLatestDateFromDisk() -> Date {
+        let interval = UserDefaults.standard.double(forKey: "WatchStateSnapshot.latest")
+        return Date(timeIntervalSince1970: interval)
+    }
+}

+ 243 - 156
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -77,7 +77,6 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             .store(in: &subscriptions)
 
         registerHandlers()
-        subscribeToBolusProgress()
     }
 
     private func registerHandlers() {
@@ -396,30 +395,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     // MARK: - Send to Watch
 
-    /// Sends the state of type WatchState to the connected Watch
-    /// - Parameter state: Current WatchState containing glucose data to be sent
-    @MainActor func sendDataToWatch(_ state: WatchState) async {
-        guard let session = session else { return }
-
-        guard session.isPaired else {
-            debug(.watchManager, "⌚️❌ No Watch is paired")
-            return
-        }
-
-        guard session.isWatchAppInstalled else {
-            debug(.watchManager, "⌚️❌ Trio Watch app is")
-            return
-        }
-
-        guard session.activationState == .activated else {
-            let activationStateString = "\(session.activationState)"
-            debug(.watchManager, "⌚️ Watch session activationState = \(activationStateString). Reactivating...")
-            session.activate()
-            return
-        }
-
-        let message: [String: Any] = [
-            WatchMessageKeys.date: Date().timeIntervalSince1970,
+    func watchStateToDictionary(from state: WatchState) -> [String: Any] {
+        [
+            WatchMessageKeys.date: state.date.timeIntervalSince1970,
             WatchMessageKeys.currentGlucose: state.currentGlucose ?? "--",
             WatchMessageKeys.currentGlucoseColorString: state.currentGlucoseColorString ?? "#ffffff",
             WatchMessageKeys.trend: state.trend ?? "",
@@ -453,8 +431,41 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             WatchMessageKeys.maxFat: state.maxFat,
             WatchMessageKeys.maxProtein: state.maxProtein,
             WatchMessageKeys.bolusIncrement: state.bolusIncrement,
-            WatchMessageKeys.confirmBolusFaster: state.confirmBolusFaster
+            WatchMessageKeys.confirmBolusFaster: state.confirmBolusFaster,
+            WatchMessageKeys.units: state.units.rawValue
         ]
+    }
+
+    /// Sends the state of type WatchState to the connected Watch
+    /// - Parameter state: Current WatchState containing glucose data to be sent
+    @MainActor func sendDataToWatch(_ state: WatchState) async {
+        guard let session = session else { return }
+
+        guard session.isPaired else {
+            debug(.watchManager, "⌚️❌ No Watch is paired")
+            return
+        }
+
+        guard session.isWatchAppInstalled else {
+            debug(.watchManager, "⌚️❌ Trio Watch app is")
+            return
+        }
+
+        guard session.activationState == .activated else {
+            let activationStateString = "\(session.activationState)"
+            debug(.watchManager, "⌚️ Watch session activationState = \(activationStateString). Reactivating...")
+            session.activate()
+            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
+        }
+
+        let message: [String: Any] = watchStateToDictionary(from: state)
 
         // if session is reachable, it means watch App is in the foreground -> send watchState as message
         // if session is not reachable, it means it's in background -> send watchState as userInfo
@@ -462,12 +473,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             session.sendMessage([WatchMessageKeys.watchState: message], replyHandler: nil) { error in
                 debug(.watchManager, "❌ Error sending watch state: \(error.localizedDescription)")
             }
+            WatchStateSnapshot.saveLatestDateToDisk(state.date)
         } else {
+            WatchStateSnapshot.saveLatestDateToDisk(state.date)
             session.transferUserInfo([WatchMessageKeys.watchState: message])
+            debug(.watchManager, "📤 Transferred new WatchState snapshot via userInfo")
         }
     }
 
-    func sendAcknowledgment(toWatch success: Bool, message: String = "") {
+    func sendAcknowledgment(toWatch success: Bool, message: String = "", ackCode: AcknowledgmentCode) {
         guard let session = session, session.isReachable else {
             debug(.watchManager, "⌚️ Watch not reachable for acknowledgment")
             return
@@ -475,7 +489,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
         let ackMessage: [String: Any] = [
             WatchMessageKeys.acknowledged: success,
-            WatchMessageKeys.message: message
+            WatchMessageKeys.message: message,
+            WatchMessageKeys.ackCode: ackCode.rawValue
         ]
 
         session.sendMessage(ackMessage, replyHandler: nil) { error in
@@ -503,7 +518,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
         DispatchQueue.main.async { [weak self] in
-            // Check Watch State Update Request first
+            if let logs = message["watchLogs"] as? String {
+                SimpleLogReporter.appendToWatchLog(logs)
+            }
+
             if let requestWatchUpdate = message[WatchMessageKeys.requestWatchUpdate] as? String,
                requestWatchUpdate == WatchMessageKeys.watchState
             {
@@ -542,6 +560,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 self?.handleCombinedRequest(bolusAmount: Decimal(bolusAmount), carbsAmount: Decimal(carbsAmount), date: date)
             } else {
                 debug(.watchManager, "📱 Invalid or incomplete data received from watch. Received:  \(message)")
+                // Acknowledge failure
+                self?.sendAcknowledgment(
+                    toWatch: false,
+                    message: "Error! Invalid or incomplete data received from watch.",
+                    ackCode: .genericFailure
+                )
             }
 
             if message[WatchMessageKeys.cancelOverride] as? Bool == true {
@@ -564,20 +588,6 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 self?.handleCancelTempTarget()
             }
 
-            // Handle bolus cancellation
-            if message[WatchMessageKeys.cancelBolus] as? Bool == true {
-                Task {
-                    await self?.apsManager.cancelBolus { [self] success, message in
-                        // Acknowledge success or error of bolus
-                        self?.sendAcknowledgment(toWatch: success, message: message)
-                    }
-                    debug(.watchManager, "📱 Bolus cancelled from watch")
-
-                    // perform determine basal sync, otherwise you could end up with too much IOB when opening the calculator again
-                    try await self?.apsManager.determineBasalSync()
-                }
-            }
-
             if message[WatchMessageKeys.requestBolusRecommendation] as? Bool == true {
                 let carbs = message[WatchMessageKeys.carbs] as? Int ?? 0
 
@@ -598,7 +608,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     ]
 
                     if let session = self.session, session.isReachable {
-                        print("📱 Sending recommendedBolus: \(result.insulinCalculated)")
+                        debug(.watchManager, "📱 Sending recommendedBolus: \(result.insulinCalculated)")
                         session.sendMessage(recommendationMessage, replyHandler: nil)
                     }
                 }
@@ -607,6 +617,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         }
     }
 
+    func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
+        if let logs = userInfo["watchLogs"] as? String {
+            SimpleLogReporter.appendToWatchLog(logs)
+        }
+    }
+
     #if os(iOS)
         func sessionDidBecomeInactive(_: WCSession) {}
         func sessionDidDeactivate(_ session: WCSession) {
@@ -637,7 +653,11 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             await apsManager.enactBolus(amount: Double(amount), isSMB: false) { success, message in
                 // Acknowledge success or error of bolus
-                self.sendAcknowledgment(toWatch: success, message: message)
+                self.sendAcknowledgment(
+                    toWatch: success,
+                    message: message,
+                    ackCode: success == true ? .genericSuccess : .genericFailure
+                )
             }
             debug(.watchManager, "📱 Enacted bolus via APS Manager: \(amount)U")
         }
@@ -661,7 +681,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 carbEntry.isUploadedToNS = false
 
                 do {
-                    guard context.hasChanges else { return }
+                    guard context.hasChanges else {
+                        // Acknowledge failure
+                        self.sendAcknowledgment(
+                            toWatch: false,
+                            message: "Error! Something went wrong when processing your request.",
+                            ackCode: .genericFailure
+                        )
+                        return
+                    }
                     try context.save()
                     debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
 
@@ -671,13 +699,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         message: String(
                             localized: "Carbs logged successfully.",
                             comment: "Success message sent to watch when carbs are logged successfully"
-                        )
+                        ),
+                        ackCode: .carbsLogged
                     )
                 } catch {
                     debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
 
                     // Acknowledge failure
-                    self.sendAcknowledgment(toWatch: false, message: "Error logging carbs")
+                    self.sendAcknowledgment(toWatch: false, message: "Error logging carbs", ackCode: .genericFailure)
                 }
             }
         }
@@ -696,7 +725,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 // Notify Watch: "Saving carbs..."
                 self.sendAcknowledgment(
                     toWatch: true,
-                    message: String(localized: "Saving Carbs...", comment: "Successful message sent to watch when saving carbs")
+                    message: String(localized: "Saving Carbs...", comment: "Successful message sent to watch when saving carbs"),
+                    ackCode: .savingCarbs
                 )
 
                 // Save carbs entry in Core Data
@@ -709,7 +739,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                     carbEntry.isUploadedToNS = false
 
-                    guard context.hasChanges else { return }
+                    guard context.hasChanges else {
+                        // Acknowledge failure
+                        self.sendAcknowledgment(
+                            toWatch: false,
+                            message: "Error! Something went wrong when processing your request.",
+                            ackCode: .genericFailure
+                        )
+                        return
+                    }
                     try context.save()
                     debug(.watchManager, "📱 Saved carbs from watch: \(carbsAmount) g at \(date)")
                 }
@@ -720,14 +758,19 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     message: String(
                         localized: "Enacting bolus...",
                         comment: "Successful message sent to watch when enacting bolus"
-                    )
+                    ),
+                    ackCode: .enactingBolus
                 )
 
                 // Enact bolus via APS Manager
                 let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
                 await apsManager.enactBolus(amount: bolusDouble, isSMB: false) { success, message in
                     // Acknowledge success or error of bolus
-                    self.sendAcknowledgment(toWatch: success, message: message)
+                    self.sendAcknowledgment(
+                        toWatch: success,
+                        message: message,
+                        ackCode: success == true ? .genericSuccess : .genericFailure
+                    )
                 }
                 debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
                 // Notify Watch: "Carbs and bolus logged successfully"
@@ -736,12 +779,13 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     message: String(
                         localized: "Carbs and Bolus logged successfully.",
                         comment: "Successful message sent to watch when logging carbs and bolus"
-                    )
+                    ),
+                    ackCode: .comboComplete
                 )
 
             } catch {
                 debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
-                sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus")
+                sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus", ackCode: .genericFailure)
             }
         }
     }
@@ -760,7 +804,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         activeOverride.enabled = false
 
                         do {
-                            guard context.hasChanges else { return }
+                            guard context.hasChanges else {
+                                // Acknowledge failure
+                                self.sendAcknowledgment(
+                                    toWatch: false,
+                                    message: "Error! Something went wrong when processing your request.",
+                                    ackCode: .genericFailure
+                                )
+                                return
+                            }
                             try context.save()
                             debug(.watchManager, "📱 Successfully stopped override")
 
@@ -771,14 +823,26 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                             )
 
                             // Acknowledge cancellation success
-                            self.sendAcknowledgment(toWatch: true, message: "Stopped Override successfully.")
+                            self.sendAcknowledgment(
+                                toWatch: true,
+                                message: "Stopped Override successfully.",
+                                ackCode: .overrideStopped
+                            )
                         } catch {
                             debug(.watchManager, "❌ Error cancelling override: \(error.localizedDescription)")
                             // Acknowledge cancellation error
-                            self.sendAcknowledgment(toWatch: false, message: "Error stopping Override.")
+                            self.sendAcknowledgment(toWatch: false, message: "Error stopping Override.", ackCode: .genericFailure)
                         }
                     }
                 }
+            } else {
+                debug(.watchManager, "❌ No active override found.")
+                self.sendAcknowledgment(
+                    toWatch: false,
+                    message: "No active override found.",
+                    ackCode: .genericFailure
+                )
+                return
             }
         }
     }
@@ -787,49 +851,91 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             let context = CoreDataStack.shared.newTaskContext()
 
+            debug(.watchManager, "📱 Fetching all override presets...")
+
             // Fetch all presets to find the one to activate
             let presetIds = try await overrideStorage.fetchForOverridePresets()
             let presets: [OverrideStored] = try await CoreDataStack.shared
                 .getNSManagedObject(with: presetIds, context: context)
 
-            // Check for active override
-            if let activeOverrideId = try await overrideStorage.fetchLatestActiveOverride() {
-                let activeOverride = await context.perform {
-                    context.object(with: activeOverrideId) as? OverrideStored
-                }
+            debug(.watchManager, "📱 Checking for active override...")
 
-                // Deactivate if exists
-                if let override = activeOverride {
-                    await context.perform {
-                        override.enabled = false
+            do {
+                // Check for active override
+                if let activeOverrideId = try await overrideStorage.fetchLatestActiveOverride() {
+                    let activeOverride = await context.perform {
+                        context.object(with: activeOverrideId) as? OverrideStored
                     }
+
+                    // Deactivate, if necessary
+                    if let override = activeOverride {
+                        await context.perform {
+                            override.enabled = false
+                        }
+                    }
+                } else {
+                    debug(.watchManager, "📱 Currently no override is active... proceeding to activate override: \(presetName)")
                 }
+            } catch {
+                debug(.watchManager, "❌ Error while checking for active override: \(error.localizedDescription)")
+                self.sendAcknowledgment(
+                    toWatch: false,
+                    message: "Failed to load active override.",
+                    ackCode: .genericFailure
+                )
+                return
             }
 
             // Activate the selected preset
             await context.perform {
-                if let presetToActivate = presets.first(where: { $0.name == presetName }) {
-                    presetToActivate.enabled = true
-                    presetToActivate.date = Date()
+                guard let presetToActivate = presets
+                    .first(where: { $0.name?.trimmingCharacters(in: .whitespacesAndNewlines) == presetName })
+                else {
+                    debug(.watchManager, "❌ No matching preset found for name: \"\(presetName)\" in \(presets.map(\.name))")
+                    self.sendAcknowledgment(
+                        toWatch: false,
+                        message: "Preset not found: \(presetName)",
+                        ackCode: .genericFailure
+                    )
+                    return
+                }
 
-                    do {
-                        guard context.hasChanges else { return }
-                        try context.save()
-                        debug(.watchManager, "📱 Successfully activated override: \(presetName)")
+                presetToActivate.enabled = true
+                presetToActivate.date = Date()
 
-                        // Send notification to update Adjustments UI
-                        Foundation.NotificationCenter.default.post(
-                            name: .didUpdateOverrideConfiguration,
-                            object: nil
+                do {
+                    guard context.hasChanges else {
+                        // Acknowledge failure
+                        self.sendAcknowledgment(
+                            toWatch: false,
+                            message: "Error! Something went wrong when processing your request.",
+                            ackCode: .genericFailure
                         )
-
-                        // Acknowledge activation success
-                        self.sendAcknowledgment(toWatch: true, message: "Started Override \"\(presetName)\" successfully.")
-                    } catch {
-                        debug(.watchManager, "❌ Error activating override: \(error.localizedDescription)")
-                        // Acknowledge activation error
-                        self.sendAcknowledgment(toWatch: false, message: "Error activating Override \"\(presetName)\".")
+                        return
                     }
+                    try context.save()
+                    debug(.watchManager, "📱 Successfully activated override: \(presetName)")
+
+                    // Send notification to update Adjustments UI
+                    Foundation.NotificationCenter.default.post(
+                        name: .didUpdateOverrideConfiguration,
+                        object: nil
+                    )
+
+                    // Acknowledge activation success
+                    self.sendAcknowledgment(
+                        toWatch: true,
+                        message: "Started Override \"\(presetName)\" successfully.",
+                        ackCode: .overrideStarted
+                    )
+                } catch {
+                    debug(.watchManager, "❌ Error activating override: \(error.localizedDescription)")
+                    // Acknowledge activation error
+                    self.sendAcknowledgment(
+                        toWatch: false,
+                        message: "Error activating Override \"\(presetName)\".",
+                        ackCode: .genericFailure
+                    )
                 }
             }
         }
@@ -865,7 +971,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     presetToActivate.date = Date()
 
                     do {
-                        guard context.hasChanges else { return }
+                        guard context.hasChanges else {
+                            // Acknowledge failure
+                            self.sendAcknowledgment(
+                                toWatch: false,
+                                message: "Error! Something went wrong when processing your request.",
+                                ackCode: .genericFailure
+                            )
+                            return
+                        }
                         try context.save()
                         debug(.watchManager, "📱 Successfully activated temp target: \(presetName)")
 
@@ -897,11 +1011,19 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         )
 
                         // Acknowledge activation success
-                        self.sendAcknowledgment(toWatch: true, message: "Started Temp Target \"\(presetName)\" successfully.")
+                        self.sendAcknowledgment(
+                            toWatch: true,
+                            message: "Started Temp Target \"\(presetName)\" successfully.",
+                            ackCode: .tempTargetStarted
+                        )
                     } catch {
                         debug(.watchManager, "❌ Error activating temp target: \(error.localizedDescription)")
                         // Acknowledge activation error
-                        self.sendAcknowledgment(toWatch: false, message: "Error activating Temp Target \"\(presetName)\".")
+                        self.sendAcknowledgment(
+                            toWatch: false,
+                            message: "Error activating Temp Target \"\(presetName)\".",
+                            ackCode: .genericFailure
+                        )
                     }
                 }
             }
@@ -922,7 +1044,15 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         activeTempTarget.enabled = false
 
                         do {
-                            guard context.hasChanges else { return }
+                            guard context.hasChanges else {
+                                // Acknowledge failure
+                                self.sendAcknowledgment(
+                                    toWatch: false,
+                                    message: "Error! Something went wrong when processing your request.",
+                                    ackCode: .genericFailure
+                                )
+                                return
+                            }
                             try context.save()
                             debug(.watchManager, "📱 Successfully cancelled temp target")
 
@@ -936,83 +1066,25 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                             )
 
                             // Acknowledge cancellation success
-                            self.sendAcknowledgment(toWatch: true, message: "Stopped Temp Target successfully.")
+                            self.sendAcknowledgment(
+                                toWatch: true,
+                                message: "Stopped Temp Target successfully.",
+                                ackCode: .tempTargetStopped
+                            )
                         } catch {
                             debug(.watchManager, "❌ Error stopping temp target: \(error.localizedDescription)")
                             // Acknowledge cancellation error
-                            self.sendAcknowledgment(toWatch: false, message: "Error stopping Temp Target.")
+                            self.sendAcknowledgment(
+                                toWatch: false,
+                                message: "Error stopping Temp Target.",
+                                ackCode: .genericFailure
+                            )
                         }
                     }
                 }
             }
         }
     }
-
-    /// Subscribes to bolus progress updates and sends progress or cancellation messages to the Watch
-    private func subscribeToBolusProgress() {
-        var wasBolusActive = false
-
-        apsManager.bolusProgress
-            .receive(on: DispatchQueue.main)
-            .sink { [weak self] progress in
-                if let progress = progress {
-                    wasBolusActive = true
-                    Task {
-                        await self?.sendBolusProgressToWatch(progress: progress)
-                    }
-                } else if wasBolusActive {
-                    // Only if a bolus was previously active and now nil is received,
-                    // the bolus was cancelled
-                    wasBolusActive = false
-                    self?.activeBolusAmount = 0.0
-
-                    debug(.watchManager, "📱 Bolus cancelled from phone")
-                    self?.sendBolusCanceledMessageToWatch()
-                }
-            }
-            .store(in: &subscriptions)
-    }
-
-    /// Sends bolus progress updates to the Watch
-    /// - Parameter progress: The current bolus progress as a Decimal
-    private func sendBolusProgressToWatch(progress: Decimal?) async {
-        guard let session = session, let progress = progress, let pumpManager = apsManager.pumpManager else { return }
-
-        let message: [String: Any] = [
-            WatchMessageKeys.bolusProgressTimestamp: Date().timeIntervalSince1970,
-            WatchMessageKeys.bolusProgress: Double(truncating: progress as NSNumber),
-            WatchMessageKeys.activeBolusAmount: activeBolusAmount,
-            WatchMessageKeys.deliveredAmount: pumpManager
-                .roundToSupportedBolusVolume(units: activeBolusAmount * Double(truncating: progress as NSNumber))
-        ]
-        // If the session is not yet activated, try to activate
-        if session.activationState != .activated {
-            session.activate()
-            // Then, queue data for eventual delivery in the background
-            session.transferUserInfo(message)
-            return
-        }
-
-        // If we reach here, session should be .activated
-        if session.isReachable {
-            // Real-time ephemeral
-            session.sendMessage(message, replyHandler: nil) { error in
-                debug(.watchManager, "❌ Error sending bolus progress: \(error.localizedDescription)")
-            }
-        } else {
-            // Fallback to be double safe: queue userInfo for eventual delivery
-            session.transferUserInfo(message)
-        }
-    }
-
-    private func sendBolusCanceledMessageToWatch() {
-        if let session = session, session.isReachable {
-            let message: [String: Any] = [WatchMessageKeys.bolusCanceled: true]
-            session.sendMessage(message, replyHandler: nil) { error in
-                debug(.watchManager, "❌ Error sending bolus cancellation to watch: \(error.localizedDescription)")
-            }
-        }
-    }
 }
 
 // TODO: - is there a better approach than setting up the watch state every time a setting has changed?
@@ -1087,3 +1159,18 @@ extension BaseWatchManager {
         return nil
     }
 }
+
+extension BaseWatchManager {
+    enum AcknowledgmentCode: String, Codable {
+        case savingCarbs = "saving_carbs"
+        case enactingBolus = "enacting_bolus"
+        case comboComplete = "combo_complete"
+        case carbsLogged = "carbs_logged"
+        case overrideStarted = "override_started"
+        case overrideStopped = "override_stopped"
+        case tempTargetStarted = "temp_target_started"
+        case tempTargetStopped = "temp_target_stopped"
+        case genericSuccess = "success"
+        case genericFailure = "failure"
+    }
+}