Ver código fonte

Merge pull request #354 from dsnallfot/override-ns-rendering

Force Nightscout main chart re-rendering of changed override durations
Deniz Cengiz 1 ano atrás
pai
commit
47cec88919

+ 61 - 0
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -12,6 +12,11 @@ protocol OverrideStorage {
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
+    func checkIfShouldDeleteNightscoutOverrideEntry(
+        forCreatedAt createdAtString: String,
+        newDuration: Int?,
+        using nightscout: NightscoutAPI
+    ) async throws
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride]
     func fetchLatestActiveOverride() async throws -> NSManagedObjectID?
 }
@@ -293,6 +298,62 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
     }
 
+    /// This check is needed to force re-rendering of overrides in the Nightscout main chart
+    /// if the override duration has changed (cancelled, customized or replaced with other override),
+    /// since just updating durations in existing entries doesn't trigger re-rendering.
+    func checkIfShouldDeleteNightscoutOverrideEntry(
+        forCreatedAt createdAtString: String,
+        newDuration: Int?,
+        using nightscout: NightscoutAPI
+    ) async throws {
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+
+        guard let jsonDate = formatter.date(from: createdAtString) else {
+            debug(.nightscout, "Could not parse override created_at string: \(createdAtString)")
+            return
+        }
+
+        /// Define a tolerance window (in seconds)
+        /// This is neccessary to handle small rounding/conversion time differences
+        /// when comparing dates between core data and NightscoutExercise json
+        let tolerance: TimeInterval = 0.1
+        let lowerBound = jsonDate.addingTimeInterval(-tolerance)
+        let upperBound = jsonDate.addingTimeInterval(tolerance)
+
+        /// Build a predicate to fetch a stored override (from OverrideStored) whose date is within the tolerance window.
+        let predicate = NSPredicate(format: "date >= %@ AND date <= %@", lowerBound as NSDate, upperBound as NSDate)
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: context,
+            predicate: predicate,
+            key: "date",
+            ascending: false
+        )
+
+        let storedOverride: NightscoutExercise? = await context.perform {
+            guard let fetched = results as? [OverrideStored],
+                  let record = fetched.first,
+                  let recordDate = record.date else { return nil }
+            let duration = record.indefinite ? 43200 : record.duration ?? 0
+            return NightscoutExercise(
+                duration: Int(truncating: duration),
+                eventType: OverrideStored.EventType.nsExercise,
+                createdAt: recordDate,
+                enteredBy: NightscoutExercise.local,
+                notes: record.name ?? String(localized: "Custom Override"),
+                id: UUID(uuidString: record.id ?? UUID().uuidString)
+            )
+        }
+
+        if let existing = storedOverride {
+            // Only delete existing nightscout entries if the durations differ.
+            if let existingDuration = existing.duration, let newDuration = newDuration, existingDuration != newDuration {
+                try await nightscout.deleteNightscoutOverride(withCreatedAt: createdAtString)
+            }
+        }
+    }
+
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,

+ 9 - 2
Trio/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift

@@ -23,8 +23,15 @@ struct OverrideView: ChartContent {
                 attribute: "duration",
                 context: viewContext
             ) ?? 0
-            let end: Date = duration != 0 ? start.addingTimeInterval(duration) : start
-                .addingTimeInterval(60 * 60 * 24 * 30) // handle infinite overrides -> 60s x 60m x 24h x 30d = 30 days duration
+            let end: Date = {
+                if override.indefinite {
+                    return start.addingTimeInterval(60 * 60 * 24 * 30)
+                } else if duration != 0 {
+                    return start.addingTimeInterval(duration)
+                } else {
+                    return start.addingTimeInterval(60 * 60 * 24 * 30)
+                }
+            }()
 
             let target = getOverrideTarget(override: override)
 

+ 33 - 0
Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -435,6 +435,39 @@ extension NightscoutAPI {
         }
     }
 
+    /// The delete func is needed to force re-rendering of overrides with changed durations in Nightscout main chart
+    /// since just updating durations in existing entries doesn't trigger re-rendering.
+    func deleteNightscoutOverride(withCreatedAt createdAt: String) async throws {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [
+            URLQueryItem(name: "find[created_at][$eq]", value: createdAt)
+        ]
+
+        guard let url = components.url else {
+            throw URLError(.badURL)
+        }
+
+        var request = URLRequest(url: url)
+        request.timeoutInterval = Config.timeout
+        request.httpMethod = "DELETE"
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        let (_, response) = try await URLSession.shared.data(for: request)
+        if let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) {
+        } else {
+            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
+            debug(.nightscout, "Failed to delete override with created_at: \(createdAt). HTTP status code: \(statusCode)")
+            throw URLError(.badServerResponse)
+        }
+    }
+
     func uploadOverrides(_ overrides: [NightscoutExercise]) async throws {
         var components = URLComponents()
         components.scheme = url.scheme

+ 38 - 3
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -1090,12 +1090,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
 
         do {
-            for chunk in overrides.chunks(ofCount: 100) {
+            var processedOverrides: [NightscoutExercise] = []
+
+            for override in overrides {
+                guard let createdAtString = override.created_at as? String else {
+                    continue
+                }
+
+                /// Check for an existing stored override and delete if needed
+                /// This is neccessary to delete original entry in NS when a running override gets customized with a new duration.
+                try await overridesStorage.checkIfShouldDeleteNightscoutOverrideEntry(
+                    forCreatedAt: createdAtString,
+                    newDuration: override.duration,
+                    using: nightscout
+                )
+
+                processedOverrides.append(override)
+            }
+
+            for chunk in processedOverrides.chunks(ofCount: 100) {
                 try await nightscout.uploadOverrides(Array(chunk))
             }
 
             // If successful, update the isUploadedToNS property of the OverrideStored objects
-            await updateOverridesAsUploaded(overrides)
+            await updateOverridesAsUploaded(processedOverrides)
 
             debug(.nightscout, "Overrides uploaded")
         } catch {
@@ -1131,7 +1149,24 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
 
         do {
-            for chunk in overrideRuns.chunks(ofCount: 100) {
+            var processedOverrideRuns: [NightscoutExercise] = []
+            for overrideRun in overrideRuns {
+                guard let createdAtString = overrideRun.created_at as? String else {
+                    continue
+                }
+
+                /// Check for an existing stored override and delete if needed
+                /// This is neccessary when a running override is cancelled, or replaced with a new override, before its duration is over.
+                try await overridesStorage.checkIfShouldDeleteNightscoutOverrideEntry(
+                    forCreatedAt: createdAtString,
+                    newDuration: overrideRun.duration,
+                    using: nightscout
+                )
+
+                processedOverrideRuns.append(overrideRun)
+            }
+
+            for chunk in processedOverrideRuns.chunks(ofCount: 100) {
                 try await nightscout.uploadOverrides(Array(chunk))
             }