Bläddra i källkod

Address PR feedback by @kingst
* Change WatchLogger from Class to Actor implementation
* Lower flushInterval
* Increase max entry size and flushSizeThreshold (300 to 3,000 and 75 to 1,000
* Ensure that whenever there is an error, logs are persisted on file
* Ensure whenever watch is flushing and sendMessage fails, logs are persisted on file
* Ensure that logs are always flushed when watch app goes to background

Deniz Cengiz 1 år sedan
förälder
incheckning
285cb485b8

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

+ 57 - 38
Trio Watch App Extension/WatchLogger.swift

@@ -1,81 +1,99 @@
-//
-//  WatchLogger.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 18.04.25.
-//
 import Foundation
 import WatchConnectivity
 
-final class WatchLogger {
+actor WatchLogger {
     static let shared = WatchLogger()
 
     private var logs: [String] = []
-    private let maxEntries = 300
-    private let flushInterval: TimeInterval = 7.5 * 60
-    private let flushSizeThreshold = 75
-
+    private let maxEntries = 3000
+    private let flushInterval: TimeInterval = 3 * 60
+    private let flushSizeThreshold = 1000
     private var lastFlush = Date()
+
     private let session = WCSession.default
+    private var timerTask: Task<Void, Never>?
 
     private init() {
-        Timer.scheduledTimer(withTimeInterval: flushInterval, repeats: true) { _ in
-            self.flushIfNeeded(force: false)
+        Task {
+            await startFlushTimer()
         }
     }
 
     private var dateFormatter: DateFormatter {
-        let dateFormatter = DateFormatter()
-        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
-        return 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) {
+    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)
 
+        logs.append(entry)
         if logs.count > maxEntries {
-            logs.removeFirst()
+            logs.removeFirst(logs.count - maxEntries)
         }
 
         print(entry)
-        flushIfNeeded(force: force)
+        await flushIfNeeded(force: force)
     }
 
-    func flushIfNeeded(force: Bool = false) {
+    func flushIfNeeded(force: Bool = false) async {
         let now = Date()
         let shouldFlush = force || now.timeIntervalSince(lastFlush) >= flushInterval || logs.count >= flushSizeThreshold
 
         if shouldFlush {
-            flushToPhone()
+            await flushToPhone()
+        } else {
+            await log("⌚️ Skipping flush — force: \(force), logs: \(logs.count), interval: \(now.timeIntervalSince(lastFlush))")
         }
     }
 
-    private func flushToPhone() {
-        guard !logs.isEmpty else { return }
+    private func flushToPhone() async {
+        guard !logs.isEmpty else {
+            await log("⌚️ No logs to flush")
+            return
+        }
 
-        let payload: [String: Any] = [
-            "watchLogs": logs.joined(separator: "\n")
-        ]
+        let payload: [String: Any] = ["watchLogs": logs.joined(separator: "\n")]
 
         if session.activationState != .activated {
             session.activate()
         }
 
         if session.isReachable {
+            await log("⌚️ Sending logs to phone: \(logs.count) entries")
             session.sendMessage(payload, replyHandler: nil) { error in
-                print("⌚️ Failed to flush logs to phone via sendMessage: \(error.localizedDescription)")
-                self.persistLogsLocally()
+                Task {
+                    await self.log("⌚️ Failed to flush logs to phone: \(error.localizedDescription)")
+                    await self.persistLogsLocally()
+                }
             }
+        } else {
+            await log("⌚️ Phone not reachable, persisting logs locally")
+            await persistLogsLocally()
         }
 
         lastFlush = Date()
         logs.removeAll()
     }
 
-    func persistLogsLocally() {
+    func persistLogsLocally() async {
         let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
             .appendingPathComponent("logs", isDirectory: true)
 
@@ -85,7 +103,6 @@ final class WatchLogger {
         let previousLogFile = logDir.appendingPathComponent("watch_log_prev.txt")
         let startOfDay = Calendar.current.startOfDay(for: Date())
 
-        // Rotate if necessary
         if let attributes = try? FileManager.default.attributesOfItem(atPath: logFile.path),
            let creationDate = attributes[.creationDate] as? Date,
            creationDate < startOfDay
@@ -105,11 +122,11 @@ final class WatchLogger {
                 try? data.write(to: logFile)
             }
         }
+
+        await log("⌚️ Persisted \(logs.count) logs locally to \(logFile.lastPathComponent)")
     }
 
-    /// Optional recovery mechanism:
-    /// Use this on startup to attempt flushing local file logs to phone
-    func flushPersistedLogs() {
+    func flushPersistedLogs() async {
         let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
             .appendingPathComponent("logs", isDirectory: true)
         let logFile = logDir.appendingPathComponent("watch_log.txt")
@@ -119,20 +136,22 @@ final class WatchLogger {
               !logString.isEmpty
         else { return }
 
-        let payload: [String: Any] = [
-            "watchLogs": logString
-        ]
+        let payload: [String: Any] = ["watchLogs": logString]
 
         if session.activationState != .activated {
             session.activate()
         }
 
         if session.isReachable {
+            await log("⌚️ Sending persisted logs to phone")
             session.sendMessage(payload, replyHandler: nil) { error in
-                print("⌚️ Failed to resend persisted logs: \(error.localizedDescription)")
+                Task {
+                    await self.log("⌚️ Failed to resend persisted logs: \(error.localizedDescription)")
+                }
             }
             try? FileManager.default.removeItem(at: logFile)
         } else {
+            await log("⌚️ Transferring persisted logs via userInfo")
             _ = session.transferUserInfo(payload)
             try? FileManager.default.removeItem(at: logFile)
         }

+ 110 - 32
Trio Watch App Extension/WatchState+Requests.swift

@@ -9,23 +9,31 @@ extension WatchState {
     ///   - amount: The insulin amount to be delivered
     func sendBolusRequest(_ amount: Decimal) {
         guard let session = session, session.isReachable else {
-            WatchLogger.shared.log("⌚️ Bolus request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Bolus request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Sending bolus request: \(amount)U")
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending bolus request: \(amount)U")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.bolus: amount
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            WatchLogger.shared.log("Error sending bolus request: \(error.localizedDescription)")
+            Task {
+                await WatchLogger.shared.log("Error sending bolus request: \(error.localizedDescription)")
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
-        WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        Task {
+            await WatchLogger.shared.log("⌚️ showCommsAnimation = true")
+        }
     }
 
     /// Sends a carbohydrate entry request to the paired iPhone
@@ -34,11 +42,15 @@ extension WatchState {
     ///   - 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 {
-            WatchLogger.shared.log("⌚️ Carbs request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Carbs request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Sending carbs request: \(amount)g at \(date)")
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending carbs request: \(amount)g at \(date)")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.carbs: amount,
@@ -46,112 +58,162 @@ extension WatchState {
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            WatchLogger.shared.log("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
-        WatchLogger.shared.log("⌚️ 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 {
-            WatchLogger.shared.log("⌚️ Cancel override request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Cancel override request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Sending cancel override request")
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending cancel override request")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.cancelOverride: true
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            WatchLogger.shared.log("⌚️ 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
-        WatchLogger.shared.log("⌚️ 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 {
-            WatchLogger.shared.log("⌚️ Activate override request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Activate override request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Sending activate override request for preset: \(presetName)")
+        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
-            WatchLogger.shared.log("⌚️ 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
-        WatchLogger.shared.log("⌚️ 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 {
-            WatchLogger.shared.log("⌚️ Cancel temp target request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Cancel temp target request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Sending cancel temp target request")
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending cancel temp target request")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.cancelTempTarget: true
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            WatchLogger.shared.log("⌚️ 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
-        WatchLogger.shared.log("⌚️ 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 {
-            WatchLogger.shared.log("⌚️ Activate temp target request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Activate temp target request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Sending activate temp target request for preset: \(presetName)")
+        Task {
+            await WatchLogger.shared.log("⌚️ Sending activate temp target request for preset: \(presetName)")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.activateTempTarget: presetName
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            WatchLogger.shared.log("⌚️ Error sending activate temp target 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()
+            }
         }
 
         // Display pending communication animation
         showCommsAnimation = true
-        WatchLogger.shared.log("⌚️ 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 {
-            WatchLogger.shared.log("⌚️ Bolus recommendation request aborted: session unreachable")
+            Task {
+                await WatchLogger.shared.log("⌚️ Bolus recommendation request aborted: session unreachable")
+            }
             return
         }
 
-        WatchLogger.shared.log("⌚️ Requesting bolus recommendation for carbs: \(carbsAmount)")
+        Task {
+            await WatchLogger.shared.log("⌚️ Requesting bolus recommendation for carbs: \(carbsAmount)")
+        }
 
         let message: [String: Any] = [
             WatchMessageKeys.requestBolusRecommendation: true,
@@ -159,32 +221,48 @@ extension WatchState {
         ]
 
         session.sendMessage(message, replyHandler: nil) { error in
-            WatchLogger.shared.log("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()
+            }
         }
     }
 
     func requestWatchStateUpdate() {
         guard let session = session else {
-            WatchLogger.shared.log("⌚️ No session available for state update")
+            Task {
+                await WatchLogger.shared.log("⌚️ No session available for state update")
+            }
             return
         }
 
         guard session.activationState == .activated else {
-            WatchLogger.shared.log("⌚️ Session not activated. Activating...")
+            Task {
+                await WatchLogger.shared.log("⌚️ Session not activated. Activating...")
+            }
             session.activate()
             return
         }
 
         if session.isReachable {
-            WatchLogger.shared.log("⌚️ Requesting WatchState update from iPhone")
+            Task {
+                await WatchLogger.shared.log("⌚️ Requesting WatchState update from iPhone")
+            }
 
             let message = [WatchMessageKeys.requestWatchUpdate: WatchMessageKeys.watchState]
 
             session.sendMessage(message, replyHandler: nil) { error in
-                WatchLogger.shared.log("⌚️ Error requesting WatchState update: \(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 {
-            WatchLogger.shared.log("⌚️ Phone not reachable for WatchState update")
+            Task {
+                await WatchLogger.shared.log("⌚️ Phone not reachable for WatchState update")
+            }
         }
     }
 }

+ 95 - 32
Trio Watch App Extension/WatchState.swift

@@ -87,19 +87,27 @@ import WatchConnectivity
             session.delegate = self
             session.activate()
             self.session = session
-            WatchLogger.shared.log("⌚️ WCSession setup complete.")
+            Task {
+                await WatchLogger.shared.log("⌚️ WCSession setup complete.")
+            }
         } else {
-            WatchLogger.shared.log("⌚️ 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) {
-        WatchLogger.shared.log("Handling acknowledgment: \(message), success: \(success), isFinal: \(isFinal)")
+        Task {
+            await WatchLogger.shared.log("Handling acknowledgment: \(message), success: \(success), isFinal: \(isFinal)")
+        }
 
         if success {
-            WatchLogger.shared.log("⌚️ Acknowledgment received: \(message)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Acknowledgment received: \(message)")
+            }
             acknowledgementStatus = .success
             acknowledgmentMessage = message
 
@@ -108,7 +116,9 @@ import WatchConnectivity
                 self.showCommsAnimation = false
             }
         } else {
-            WatchLogger.shared.log("⌚️ Acknowledgment failed: \(message)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Acknowledgment failed: \(message)")
+            }
 
             // Hide progress animation
             DispatchQueue.main.async {
@@ -126,7 +136,9 @@ import WatchConnectivity
             DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                 self.showAcknowledgmentBanner = false
                 self.showSyncingAnimation = false // Just ensure this is 100% set to false
-                WatchLogger.shared.log("Cleared ack banner and syncing animation")
+                Task {
+                    await WatchLogger.shared.log("Cleared ack banner and syncing animation")
+                }
             }
         }
     }
@@ -138,25 +150,35 @@ import WatchConnectivity
     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
         DispatchQueue.main.async {
             if let error = error {
-                WatchLogger.shared.log("⌚️ Watch session activation failed: \(error.localizedDescription)", force: true)
+                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 {
-                WatchLogger.shared.log("⌚️ 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
 
-                WatchLogger.shared.log("⌚️ 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]) {
-        WatchLogger.shared.log("⌚️ 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],
@@ -166,10 +188,14 @@ import WatchConnectivity
 
             // Check if it's not older than 15 min
             if date >= Date().addingTimeInterval(-15 * 60) {
-                WatchLogger.shared.log("⌚️ Handling watchState from \(date)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Handling watchState from \(date)")
+                }
                 processWatchMessage(message)
             } else {
-                WatchLogger.shared.log("⌚️ Received outdated watchState data (\(date))")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Received outdated watchState data (\(date))")
+                }
                 DispatchQueue.main.async {
                     self.showSyncingAnimation = false
                 }
@@ -184,7 +210,10 @@ import WatchConnectivity
             let ackMessage = message[WatchMessageKeys.message] as? String,
             let ackCodeRaw = message[WatchMessageKeys.ackCode] as? String
         {
-            WatchLogger.shared.log("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged), ackCode: \(ackCodeRaw)")
+            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
@@ -196,7 +225,9 @@ import WatchConnectivity
         } else if
             let recommendedBolus = message[WatchMessageKeys.recommendedBolus] as? NSNumber
         {
-            WatchLogger.shared.log("⌚️ Received recommended bolus: \(recommendedBolus)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Received recommended bolus: \(recommendedBolus)")
+            }
 
             DispatchQueue.main.async {
                 self.recommendedBolus = recommendedBolus.decimalValue
@@ -205,7 +236,9 @@ import WatchConnectivity
 
             return
         } else {
-            WatchLogger.shared.log("⌚️ Faulty data. Skipping...")
+            Task {
+                await WatchLogger.shared.log("⌚️ Faulty data. Skipping...")
+            }
             DispatchQueue.main.async {
                 self.showSyncingAnimation = false
             }
@@ -214,14 +247,18 @@ import WatchConnectivity
 
     func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
         guard let snapshot = WatchStateSnapshot(from: userInfo) else {
-            WatchLogger.shared.log("⌚️ Invalid snapshot received", force: true)
+            Task {
+                await WatchLogger.shared.log("⌚️ Invalid snapshot received", force: true)
+            }
             return
         }
 
         let lastProcessed = WatchStateSnapshot.loadLatestDateFromDisk()
 
         guard snapshot.date > lastProcessed else {
-            WatchLogger.shared.log("⌚️ Ignoring outdated or duplicate WatchState snapshot", force: true)
+            Task {
+                await WatchLogger.shared.log("⌚️ Ignoring outdated or duplicate WatchState snapshot", force: true)
+            }
             return
         }
 
@@ -234,9 +271,11 @@ import WatchConnectivity
 
     func session(_: WCSession, didFinish _: WCSessionUserInfoTransfer, error: (any Error)?) {
         if let error = error {
-            WatchLogger.shared.log("⌚️ transferUserInfo failed with error: \(error.localizedDescription)")
-            WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
-            WatchLogger.shared.persistLogsLocally()
+            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()
+            }
         }
     }
 
@@ -244,7 +283,9 @@ import WatchConnectivity
     /// Updates the local reachability status
     func sessionReachabilityDidChange(_ session: WCSession) {
         DispatchQueue.main.async {
-            WatchLogger.shared.log("⌚️ Watch reachability changed: \(session.isReachable)")
+            Task {
+                await WatchLogger.shared.log("⌚️ Watch reachability changed: \(session.isReachable)")
+            }
 
             if session.isReachable {
                 self.forceConditionalWatchStateUpdate()
@@ -269,7 +310,9 @@ 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 {
-            WatchLogger.shared.log("Forcing initial WatchState update")
+            Task {
+                await WatchLogger.shared.log("Forcing initial WatchState update")
+            }
 
             // If there's no recorded timestamp, we must force a fresh update immediately.
             showSyncingAnimation = true
@@ -279,7 +322,9 @@ import WatchConnectivity
 
         let now = Date().timeIntervalSince1970
         let secondsSinceUpdate = now - lastUpdateTimestamp
-        WatchLogger.shared.log("Time since last update: \(secondsSinceUpdate) seconds")
+        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 {
@@ -302,7 +347,9 @@ import WatchConnectivity
                     self.showSyncingAnimation = false
                 }
 
-                WatchLogger.shared.log("⌚️ Received acknowledgment: \(ackMessage), success: \(acknowledged)")
+                Task {
+                    await WatchLogger.shared.log("⌚️ Received acknowledgment: \(ackMessage), success: \(acknowledged)")
+                }
 
                 switch ackCode {
                 case .savingCarbs:
@@ -337,7 +384,9 @@ import WatchConnectivity
            let lastTimestamp = lastWatchStateUpdate,
            incomingTimestamp <= lastTimestamp
         {
-            WatchLogger.shared.log("Skipping UI update — outdated WatchState (\(incomingTimestamp))")
+            Task {
+                await WatchLogger.shared.log("Skipping UI update — outdated WatchState (\(incomingTimestamp))")
+            }
             return
         }
 
@@ -346,7 +395,9 @@ import WatchConnectivity
             self.showSyncingAnimation = true
         }
 
-        WatchLogger.shared.log("Merging new WatchState data with keys: \(newData.keys.joined(separator: ", "))")
+        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 }
@@ -356,7 +407,9 @@ import WatchConnectivity
 
         // 4) Create and schedule a new finalization
         let workItem = DispatchWorkItem { [self] in
-            WatchLogger.shared.log("⏳ Debounced update fired")
+            Task {
+                await WatchLogger.shared.log("⏳ Debounced update fired")
+            }
             self.finalizePendingData()
         }
         finalizeWorkItem = workItem
@@ -366,7 +419,9 @@ import WatchConnectivity
     /// Applies all pending data to the watch state in one shot
     private func finalizePendingData() {
         guard !pendingData.isEmpty else {
-            WatchLogger.shared.log("⚠️ finalizePendingData called with empty data")
+            Task {
+                await WatchLogger.shared.log("⚠️ finalizePendingData called with empty data")
+            }
 
             // If we have no actual data, just end syncing
             DispatchQueue.main.async {
@@ -375,7 +430,9 @@ import WatchConnectivity
             return
         }
 
-        WatchLogger.shared.log("⌚️ Finalizing pending data")
+        Task {
+            await WatchLogger.shared.log("⌚️ Finalizing pending data")
+        }
 
         // Actually set your main UI properties here
         processRawDataForWatchState(pendingData)
@@ -388,16 +445,22 @@ import WatchConnectivity
             self.showSyncingAnimation = false
         }
 
-        WatchLogger.shared.log("✅ Watch UI update complete")
+        Task {
+            await WatchLogger.shared.log("✅ Watch UI update complete")
+        }
     }
 
     /// Updates the UI properties
     private func processRawDataForWatchState(_ message: [String: Any]) {
-        WatchLogger.shared.log("Processing raw WatchState data with keys: \(message.keys.joined(separator: ", "))")
+        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
-            WatchLogger.shared.log("Updated lastWatchStateUpdate: \(timestamp)")
+            Task {
+                await WatchLogger.shared.log("Updated lastWatchStateUpdate: \(timestamp)")
+            }
         }
 
         if let currentGlucose = message[WatchMessageKeys.currentGlucose] as? String {

+ 0 - 66
Trio/Sources/Models/WatchState.swift

@@ -78,69 +78,3 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable, Decodable {
         hasher.combine(confirmBolusFaster)
     }
 }
-
-//
-///// Codable snapshot used for saving/loading WatchState to disk
-// struct WatchStateSnapshot: Codable {
-//    let state: WatchState
-//    let timestamp: TimeInterval
-//
-//    init(state: WatchState) {
-//        self.state = state
-//        self.timestamp = state.date.timeIntervalSince1970
-//    }
-// }
-//
-// extension WatchState {
-//    // MARK: - Disk Persistence
-//
-//    private static var snapshotURL: URL {
-//        let dir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.trio.watch") ?? FileManager.default.temporaryDirectory
-//        return dir.appendingPathComponent("watch_state_snapshot.json")
-//    }
-//
-//    static func loadFromDisk() -> WatchState? {
-//        let url = snapshotURL
-//        guard FileManager.default.fileExists(atPath: url.path) else {
-//            return nil
-//        }
-//
-//        do {
-//            let data = try Data(contentsOf: url)
-//            let snapshot = try JSONDecoder().decode(WatchStateSnapshot.self, from: data)
-//            return snapshot.state
-//        } catch {
-//            print("⌚️ Failed to load WatchState snapshot: \(error.localizedDescription)")
-//            return nil
-//        }
-//    }
-//
-//    static func loadLatestDateFromDisk() -> Date {
-//        let url = snapshotURL
-//        guard FileManager.default.fileExists(atPath: url.path) else {
-//            return Date(timeIntervalSince1970: 0)
-//        }
-//
-//        do {
-//            let data = try Data(contentsOf: url)
-//            let snapshot = try JSONDecoder().decode(WatchStateSnapshot.self, from: data)
-//            return Date(timeIntervalSince1970: snapshot.timestamp)
-//        } catch {
-//            print("⌚️ Failed to load timestamp from WatchState snapshot: \(error.localizedDescription)")
-//            return Date(timeIntervalSince1970: 0)
-//        }
-//    }
-//
-//
-//    func saveToDisk() {
-//        let snapshot = WatchStateSnapshot(state: self)
-//        let url = Self.snapshotURL
-//
-//        do {
-//            let data = try JSONEncoder().encode(snapshot)
-//            try data.write(to: url, options: .atomic)
-//        } catch {
-//            print("⌚️ Failed to save WatchState snapshot: \(error.localizedDescription)")
-//        }
-//    }
-// }