Pārlūkot izejas kodu

Merge branch 'dev' of https://github.com/nightscout/Trio-dev into coreDataOptimizations

polscm32 1 gadu atpakaļ
vecāks
revīzija
47549cec53
37 mainītis faili ar 333 papildinājumiem un 96 dzēšanām
  1. 14 0
      Model/Helper/AdjustmentStored+Helper.swift
  2. 0 10
      Model/Helper/OverrideStored+helper.swift
  3. 87 3
      Model/Helper/PumpEvent+helper.swift
  4. 11 1
      Trio Watch App Extension/Helper/Helper+ButtonStyles.swift
  5. 20 0
      Trio Watch App Extension/Views/BolusProgressOverlay.swift
  6. 5 0
      Trio Watch App Extension/Views/GlucoseChartView.swift
  7. 13 4
      Trio Watch App Extension/Views/TrioMainWatchView.swift
  8. 14 0
      Trio Watch App Extension/WatchState.swift
  9. 4 0
      Trio.xcodeproj/project.pbxproj
  10. 9 3
      Trio/Sources/APS/APSManager.swift
  11. 25 10
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  12. 1 1
      Trio/Sources/APS/Storage/OverrideStorage.swift
  13. 4 2
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  14. 3 3
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  15. 2 0
      Trio/Sources/Models/WatchMessageKeys.swift
  16. 9 3
      Trio/Sources/Models/WatchState.swift
  17. 3 3
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  18. 1 1
      Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  19. 0 5
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  20. 0 5
      Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  21. 13 2
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  22. 7 1
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  23. 2 0
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  24. 1 1
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  25. 7 3
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  26. 15 4
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift
  27. 1 1
      Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  28. 1 1
      Trio/Sources/Modules/Home/HomeStateModel.swift
  29. 18 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift
  30. 2 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/ForecastView.swift
  31. 2 1
      Trio/Sources/Modules/Home/View/Chart/MainChartView.swift
  32. 0 5
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  33. 0 5
      Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  34. 11 13
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  35. 0 3
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  36. 27 0
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  37. 1 0
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

+ 14 - 0
Model/Helper/AdjustmentStored+Helper.swift

@@ -0,0 +1,14 @@
+import CoreData
+import Foundation
+
+extension NSPredicate {
+    static var lastActiveAdjustmentNotYetUploadedToNightscout: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "date >= %@ AND enabled == %@ AND isUploadedToNS == %@",
+            date as NSDate,
+            true as NSNumber,
+            false as NSNumber
+        )
+    }
+}

+ 0 - 10
Model/Helper/OverrideStored+helper.swift

@@ -14,16 +14,6 @@ extension NSPredicate {
             true as NSNumber
         )
     }
-
-    static var lastActiveOverrideNotYetUploadedToNightscout: NSPredicate {
-        let date = Date.oneDayAgo
-        return NSPredicate(
-            format: "date >= %@ AND enabled == %@ AND isUploadedToNS == %@",
-            date as NSDate,
-            true as NSNumber,
-            false as NSNumber
-        )
-    }
 }
 
 extension OverrideStored {

+ 87 - 3
Model/Helper/PumpEvent+helper.swift

@@ -128,7 +128,7 @@ struct BolusDTO: Codable {
     var isExternal: Bool
     var isSMB: Bool
     var duration: Int
-    var _type: String = "Bolus"
+    var _type: String = EventType.bolus.rawValue
 }
 
 struct TempBasalDTO: Codable {
@@ -136,14 +136,14 @@ struct TempBasalDTO: Codable {
     var timestamp: String
     var temp: String
     var rate: Double
-    var _type: String = "TempBasal"
+    var _type: String = EventType.tempBasal.rawValue
 }
 
 struct TempBasalDurationDTO: Codable {
     var id: String
     var timestamp: String
     var duration: Int
-    var _type: String = "TempBasalDuration"
+    var _type: String = EventType.tempBasalDuration.rawValue
 
     private enum CodingKeys: String, CodingKey {
         case id
@@ -153,11 +153,39 @@ struct TempBasalDurationDTO: Codable {
     }
 }
 
+struct SuspendDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.pumpSuspend.rawValue
+}
+
+struct ResumeDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.pumpResume.rawValue
+}
+
+struct RewindDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.rewind.rawValue
+}
+
+struct PrimeDTO: Codable {
+    var id: String
+    var timestamp: String
+    var _type: String = EventType.prime.rawValue
+}
+
 // Mask distinct DTO subtypes with a common enum that conforms to Encodable
 enum PumpEventDTO: Encodable {
     case bolus(BolusDTO)
     case tempBasal(TempBasalDTO)
     case tempBasalDuration(TempBasalDurationDTO)
+    case suspend(SuspendDTO)
+    case resume(ResumeDTO)
+    case rewind(RewindDTO)
+    case prime(PrimeDTO)
 
     func encode(to encoder: Encoder) throws {
         switch self {
@@ -167,6 +195,14 @@ enum PumpEventDTO: Encodable {
             try tempBasal.encode(to: encoder)
         case let .tempBasalDuration(tempBasalDuration):
             try tempBasalDuration.encode(to: encoder)
+        case let .suspend(suspend):
+            try suspend.encode(to: encoder)
+        case let .resume(resume):
+            try resume.encode(to: encoder)
+        case let .rewind(rewind):
+            try rewind.encode(to: encoder)
+        case let .prime(prime):
+            try prime.encode(to: encoder)
         }
     }
 }
@@ -221,4 +257,52 @@ extension PumpEventStored {
         )
         return .tempBasalDuration(tempBasalDurationDTO)
     }
+
+    func toPumpSuspendDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpSuspend.rawValue else {
+            return nil
+        }
+
+        let suspendDTO = SuspendDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .suspend(suspendDTO)
+    }
+
+    func toPumpResumeDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.pumpResume.rawValue else {
+            return nil
+        }
+
+        let resumeDTO = ResumeDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .resume(resumeDTO)
+    }
+
+    func toRewindDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.rewind.rawValue else {
+            return nil
+        }
+
+        let rewindDTO = RewindDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .rewind(rewindDTO)
+    }
+
+    func toPrimeDTO() -> PumpEventDTO? {
+        guard let id = id, let timestamp = timestamp, let type = type, type == EventType.prime.rawValue else {
+            return nil
+        }
+
+        let primeDTO = PrimeDTO(
+            id: id,
+            timestamp: PumpEventStored.dateFormatter.string(from: timestamp)
+        )
+        return .prime(primeDTO)
+    }
 }

+ 11 - 1
Trio Watch App Extension/Helper/Helper+ButtonStyles.swift

@@ -5,6 +5,8 @@ struct WatchOSButtonStyle: ButtonStyle {
     var foregroundColor: Color = .white
     var fontSize: Font = .title2
 
+    @Environment(\.isEnabled) private var isEnabled: Bool
+
     private var fontWeight: Font.Weight {
         switch deviceType {
         case .watch40mm:
@@ -44,11 +46,19 @@ struct WatchOSButtonStyle: ButtonStyle {
     }
 
     func makeBody(configuration: Configuration) -> some View {
+        var buttonBackground: Color {
+            if isEnabled {
+                return Color.tabBar.opacity(configuration.isPressed ? 0.8 : 1.0)
+            } else {
+                return Color.tabBar.opacity(0.4)
+            }
+        }
+
         configuration.label
             .font(fontSize)
             .fontWeight(fontWeight)
             .padding(buttonPadding)
-            .background(Color.tabBar.opacity(configuration.isPressed ? 0.8 : 1.0))
+            .background(buttonBackground)
             .clipShape(Circle())
             .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
     }

+ 20 - 0
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -16,6 +16,25 @@ struct BolusProgressOverlay: View {
         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 {
@@ -45,6 +64,7 @@ struct BolusProgressOverlay: View {
                 }
                 .buttonStyle(.bordered)
                 .padding()
+                .disabled(!isWatchStateDated || !isSessionUnreachable)
             }
             .padding()
             .background(Color.black.opacity(0.9))

+ 5 - 0
Trio Watch App Extension/Views/GlucoseChartView.swift

@@ -6,6 +6,8 @@ import SwiftUI
 
 struct GlucoseChartView: View {
     let glucoseValues: [(date: Date, glucose: Double, color: Color)]
+    let minYAxisValue: Decimal
+    let maxYAxisValue: Decimal
     @State private var timeWindow: TimeWindow = .threeHours
 
     enum TimeWindow: Int {
@@ -69,6 +71,9 @@ struct GlucoseChartView: View {
                         }
                     }
                 }
+                .chartYScale(
+                    domain: minYAxisValue ... maxYAxisValue
+                )
                 .chartPlotStyle { plotContent in
                     plotContent
                         .background(

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

@@ -88,8 +88,12 @@ struct TrioMainWatchView: View {
                 }.tag(0)
 
                 // Page 2: Glucose chart
-                GlucoseChartView(glucoseValues: state.glucoseValues)
-                    .tag(1)
+                GlucoseChartView(
+                    glucoseValues: state.glucoseValues,
+                    minYAxisValue: state.minYAxisValue,
+                    maxYAxisValue: state.maxYAxisValue
+                )
+                .tag(1)
             }
             .onAppear {
                 // Hard reset variables when main view appears
@@ -135,7 +139,9 @@ struct TrioMainWatchView: View {
                     } label: {
                         Image(systemName: "clock.arrow.2.circlepath")
                             .foregroundStyle(Color.primary, isOverrideActive ? Color.primary : Color.purple)
-                    }.tint(isOverrideActive ? Color.purple : nil)
+                    }
+                    .tint(isOverrideActive ? Color.purple : nil)
+                    .disabled(!isWatchStateDated || !isSessionUnreachable)
 
                     Button {
                         showingTreatmentMenuSheet = true
@@ -145,13 +151,16 @@ struct TrioMainWatchView: View {
                     }
                     .controlSize(.large)
                     .buttonStyle(WatchOSButtonStyle(deviceType: state.deviceType))
+                    .disabled(!isWatchStateDated || !isSessionUnreachable)
 
                     Button {
                         showingTempTargetSheet = true
                     } label: {
                         Image(systemName: "target")
                             .foregroundStyle(isTempTargetActive ? Color.primary : Color.loopGreen.opacity(0.75))
-                    }.tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
+                    }
+                    .tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
+                    .disabled(!isWatchStateDated || !isSessionUnreachable)
                 }
             }
             .fullScreenCover(isPresented: $showingTreatmentMenuSheet) {

+ 14 - 0
Trio Watch App Extension/WatchState.swift

@@ -20,6 +20,8 @@ import WatchConnectivity
     var trend: String? = ""
     var delta: String? = "--"
     var glucoseValues: [(date: Date, glucose: Double, color: Color)] = []
+    var minYAxisValue: Decimal = 39
+    var maxYAxisValue: Decimal = 200
     var cob: String? = "--"
     var iob: String? = "--"
     var lastLoopTime: String? = "--"
@@ -518,6 +520,18 @@ import WatchConnectivity
             .sorted { $0.date < $1.date }
         }
 
+        if let minYAxisValue = message[WatchMessageKeys.minYAxisValue] {
+            if let decimalValue = (minYAxisValue as? NSNumber)?.decimalValue {
+                self.minYAxisValue = decimalValue
+            }
+        }
+
+        if let maxYAxisValue = message[WatchMessageKeys.maxYAxisValue] {
+            if let decimalValue = (maxYAxisValue as? NSNumber)?.decimalValue {
+                self.maxYAxisValue = decimalValue
+            }
+        }
+
         if let overrideData = message[WatchMessageKeys.overridePresets] as? [[String: Any]] {
             overridePresets = overrideData.compactMap { data in
                 guard let name = data["name"] as? String,

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -212,6 +212,7 @@
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
+		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
 		581516A42BCED84A00BF67D7 /* DebuggingIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */; };
@@ -948,6 +949,7 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingIdentifiers.swift; sourceTree = "<group>"; };
@@ -2370,6 +2372,7 @@
 		5825D1622BD405AE00F36E9B /* Helper */ = {
 			isa = PBXGroup;
 			children = (
+				49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */,
 				581516A82BCEEDF800BF67D7 /* NSPredicates.swift */,
 				583684052BD178DB00070A60 /* GlucoseStored+helper.swift */,
 				58F107732BD1A4D000B1A680 /* Determination+helper.swift */,
@@ -4000,6 +4003,7 @@
 				DD1745222C55524800211FAC /* SMBSettingsProvider.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				583684062BD178DB00070A60 /* GlucoseStored+helper.swift in Sources */,
+				49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */,
 				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,

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

@@ -22,7 +22,7 @@ protocol APSManager {
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func determineBasal() async throws -> Bool
     func determineBasalSync() async
-    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination?
+    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
@@ -433,10 +433,16 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    func simulateDetermineBasal(carbs: Decimal, iob: Decimal) async -> Determination? {
+    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
         do {
             let temp = try await fetchCurrentTempBasal(date: Date.now)
-            return try await openAPS.determineBasal(currentTemp: temp, clock: Date(), carbs: carbs, iob: iob, simulation: true)
+            return try await openAPS.determineBasal(
+                currentTemp: temp,
+                clock: Date(),
+                simulatedCarbsAmount: simulatedCarbsAmount,
+                simulatedBolusAmount: simulatedBolusAmount,
+                simulation: true
+            )
         } catch {
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"

+ 25 - 10
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -189,7 +189,10 @@ final class OpenAPS {
         }
     }
 
-    private func parsePumpHistory(_ pumpHistoryObjectIDs: [NSManagedObjectID], iob: Decimal? = nil) async -> String {
+    private func parsePumpHistory(
+        _ pumpHistoryObjectIDs: [NSManagedObjectID],
+        simulatedBolusAmount: Decimal? = nil
+    ) async -> String {
         // Return an empty JSON object if the list of object IDs is empty
         guard !pumpHistoryObjectIDs.isEmpty else { return "{}" }
 
@@ -199,9 +202,9 @@ final class OpenAPS {
             var dtos = self.loadAndMapPumpEvents(pumpHistoryObjectIDs)
 
             // Optionally add the IOB as a DTO
-            if let iob = iob {
-                let iobDTO = self.createIOBDTO(iob: iob)
-                dtos.insert(iobDTO, at: 0)
+            if let simulatedBolusAmount = simulatedBolusAmount {
+                let simulatedBolusDTO = self.createSimulatedBolusDTO(simulatedBolusAmount: simulatedBolusAmount)
+                dtos.insert(simulatedBolusDTO, at: 0)
             }
 
             // Convert the DTOs to JSON
@@ -226,12 +229,24 @@ final class OpenAPS {
             if let tempBasalDTO = event.toTempBasalDTOEnum() {
                 eventDTOs.append(tempBasalDTO)
             }
+            if let pumpSuspendDTO = event.toPumpSuspendDTO() {
+                eventDTOs.append(pumpSuspendDTO)
+            }
+            if let pumpResumeDTO = event.toPumpResumeDTO() {
+                eventDTOs.append(pumpResumeDTO)
+            }
+            if let rewindDTO = event.toRewindDTO() {
+                eventDTOs.append(rewindDTO)
+            }
+            if let primeDTO = event.toPrimeDTO() {
+                eventDTOs.append(primeDTO)
+            }
             return eventDTOs
         }
         return dtos
     }
 
-    private func createIOBDTO(iob: Decimal) -> PumpEventDTO {
+    private func createSimulatedBolusDTO(simulatedBolusAmount: Decimal) -> PumpEventDTO {
         let oneSecondAgo = Calendar.current
             .date(
                 byAdding: .second,
@@ -243,7 +258,7 @@ final class OpenAPS {
         let bolusDTO = BolusDTO(
             id: UUID().uuidString,
             timestamp: dateFormatted,
-            amount: Double(iob),
+            amount: Double(simulatedBolusAmount),
             isExternal: false,
             isSMB: true,
             duration: 0,
@@ -255,8 +270,8 @@ final class OpenAPS {
     func determineBasal(
         currentTemp: TempBasal,
         clock: Date = Date(),
-        carbs: Decimal? = nil,
-        iob: Decimal? = nil,
+        simulatedCarbsAmount: Decimal? = nil,
+        simulatedBolusAmount: Decimal? = nil,
         simulation: Bool = false
     ) async throws -> Determination? {
         debug(.openAPS, "Start determineBasal")
@@ -266,7 +281,7 @@ final class OpenAPS {
 
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs(additionalCarbs: carbs ?? 0)
+        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0)
         async let glucose = fetchAndProcessGlucose()
         async let oref2 = oref2()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
@@ -287,7 +302,7 @@ final class OpenAPS {
             reservoir,
             preferences
         ) = await (
-            try parsePumpHistory(await pumpHistoryObjectIDs, iob: iob),
+            try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
             try carbs,
             try glucose,
             try oref2,

+ 1 - 1
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -204,7 +204,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: backgroundContext,
-            predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout,
+            predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )

+ 4 - 2
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -88,7 +88,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
 
                     let newPumpEvent = PumpEventStored(context: self.context)
                     newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
+                    // restrict entry to now or past
+                    newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
                     newPumpEvent.type = PumpEvent.bolus.rawValue
                     newPumpEvent.isUploadedToNS = false
                     newPumpEvent.isUploadedToHealth = false
@@ -225,7 +226,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             // create pump event
             let newPumpEvent = PumpEventStored(context: self.context)
             newPumpEvent.id = UUID().uuidString
-            newPumpEvent.timestamp = timestamp
+            // restrict entry to now or past
+            newPumpEvent.timestamp = timestamp > Date() ? Date() : timestamp
             newPumpEvent.type = PumpEvent.bolus.rawValue
             newPumpEvent.isUploadedToNS = false
             newPumpEvent.isUploadedToHealth = false

+ 3 - 3
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -14,7 +14,7 @@ protocol TempTargetsStorage {
     func fetchScheduledTempTargets() async throws -> [NSManagedObjectID]
     func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID]
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
-    func deleteOverridePreset(_ objectID: NSManagedObjectID) async
+    func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
     func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
     func syncDate() -> Date
     func recent() -> [TempTarget]
@@ -217,7 +217,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return newTempTarget.objectID
     }
 
-    @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
+    @MainActor func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async {
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
     }
 
@@ -247,7 +247,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
-            predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout, // TODO: create adjustment predicate (OR+TT)
+            predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )

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

@@ -37,6 +37,8 @@ enum WatchMessageKeys {
     static let cob = "cob"
     static let lastLoopTime = "lastLoopTime"
     static let glucoseValues = "glucoseValues"
+    static let minYAxisValue = "minYAxisValue"
+    static let maxYAxisValue = "maxYAxisValue"
     static let overridePresets = "overridePresets"
     static let tempTargetPresets = "tempTargetPresets"
 

+ 9 - 3
Trio/Sources/Models/WatchState.swift

@@ -8,6 +8,8 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
     var trend: String?
     var delta: String?
     var glucoseValues: [WatchGlucoseObject] = []
+    var minYAxisValue: Decimal = 39.0
+    var maxYAxisValue: Decimal = 200.0
     var units: GlucoseUnits = .mgdL
     var iob: String?
     var cob: String?
@@ -17,9 +19,9 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
 
     // Safety limits
     var maxBolus: Decimal = 10.0
-    var maxCarbs: Decimal = 250
-    var maxFat: Decimal = 250
-    var maxProtein: Decimal = 250
+    var maxCarbs: Decimal = 250.0
+    var maxFat: Decimal = 250.0
+    var maxProtein: Decimal = 250.0
 
     // Pump specific dosing increment
     var bolusIncrement: Decimal = 0.05
@@ -34,6 +36,8 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
             zip(lhs.glucoseValues, rhs.glucoseValues).allSatisfy {
                 $0.0.date == $0.1.date && $0.0.glucose == $0.1.glucose && $0.0.color == $0.1.color
             } &&
+            lhs.minYAxisValue == rhs.minYAxisValue &&
+            lhs.maxYAxisValue == rhs.maxYAxisValue &&
             lhs.units == rhs.units &&
             lhs.iob == rhs.iob &&
             lhs.cob == rhs.cob &&
@@ -58,6 +62,8 @@ struct WatchState: Hashable, Equatable, Sendable, Encodable {
             hasher.combine(value.glucose)
             hasher.combine(value.color)
         }
+        hasher.combine(minYAxisValue)
+        hasher.combine(maxYAxisValue)
         hasher.combine(units)
         hasher.combine(iob)
         hasher.combine(cob)

+ 3 - 3
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -271,7 +271,7 @@ extension Adjustments.StateModel {
             )
             tempTargetStorage.saveTempTargetsToStorage([tempTarget])
         } catch {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact TempTarget Preset")
         }
     }
 
@@ -333,7 +333,7 @@ extension Adjustments.StateModel {
 
     /// Duplicates the current preset and cancels the previous one.
     @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
-        // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
+        // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom TempTarget
         guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
               tempTargetPresetToDuplicate.isPreset == true else { return }
 
@@ -364,7 +364,7 @@ extension Adjustments.StateModel {
 
     /// Deletes a Temp Target preset.
     func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
-        await tempTargetStorage.deleteOverridePreset(objectID)
+        await tempTargetStorage.deleteTempTargetPreset(objectID)
         setupTempTargetPresetsArray()
     }
 

+ 1 - 1
Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -249,7 +249,7 @@ extension AlgorithmAdvancedSettings {
                             "Min 5m Carb Impact sets the expected glucose rise from carbs over 5 minutes when absorption isn't obvious from glucose data."
                         )
                         Text(
-                            "The default value of 8 mg/dL per 5 minutes corresponds to an absorption rate of 24 g of carbs per hour."
+                            "The default is an expected \(state.units == .mgdL ? "8" : 8.formattedAsMmolL) \(state.units.rawValue)/5min. This affects how fast COB is decayed in situations when carb absorption is not visible in BG deviations. The default of \(state.units == .mgdL ? "8" : 8.formattedAsMmolL) \(state.units.rawValue)/5min corresponds to a minimum carb absorption rate of 24 g/hr at a CSF of \(state.units == .mgdL ? "4" : 4.formattedAsMmolL) \(state.units.rawValue)/g."
                         )
                         Text(
                             "This setting helps the system estimate how much glucose your body is absorbing, even when it's not immediately visible in your glucose data, ensuring more accurate insulin dosing during carb absorption."

+ 0 - 5
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -172,11 +172,6 @@ extension BasalProfileEditor {
             .navigationTitle("Basal Profile")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                         HStack {

+ 0 - 5
Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -104,11 +104,6 @@ extension CarbRatioEditor {
             .navigationTitle("Carb Ratios")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                         HStack {

+ 13 - 2
Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift

@@ -7,6 +7,7 @@ struct AddContactImageSheet: View {
 
     @ObservedObject var state: ContactImage.StateModel
 
+    @State private var contactName: String = ""
     @State private var hasHighContrast: Bool = true
     @State private var ringWidth: ContactImageEntry.RingWidth = .regular
     @State private var ringGap: ContactImageEntry.RingGap = .small
@@ -23,7 +24,7 @@ struct AddContactImageSheet: View {
     private var previewEntry: ContactImageEntry {
         ContactImageEntry(
             id: UUID(),
-            name: "", // automatically set and populated
+            name: contactName, // automatically set and populated
             layout: layout,
             ring: ring,
             primary: primary,
@@ -66,6 +67,13 @@ struct AddContactImageSheet: View {
                 .padding(.bottom)
 
                 Form {
+                    Section(
+                        header: Text("Contact Name"),
+                        content: {
+                            TextField("Enter Name (Optional)", text: $contactName)
+                        }
+                    ).listRowBackground(Color.chart)
+
                     // Layout Section
                     Section(header: Text("Style")) {
                         Picker("Layout", selection: $layout) {
@@ -232,7 +240,10 @@ struct AddContactImageSheet: View {
     private func saveNewEntry() {
         // Save the currently previewed entry
         Task {
-            await state.createAndSaveContactImage(entry: previewEntry, name: "Trio \(state.contactImageEntries.count + 1)")
+            await state.createAndSaveContactImage(
+                entry: previewEntry,
+                name: contactName.isEmpty ? "Trio \(state.contactImageEntries.count + 1)" : contactName
+            )
             dismiss()
         }
     }

+ 7 - 1
Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift

@@ -19,7 +19,6 @@ struct ContactImageDetailView: View {
     var body: some View {
         VStack {
             HStack {
-                // TODO: - make this beautiful @Dan
                 Spacer()
                 ZStack {
                     Circle()
@@ -41,6 +40,13 @@ struct ContactImageDetailView: View {
             .padding(.bottom)
 
             Form {
+                Section(
+                    header: Text("Contact Name"),
+                    content: {
+                        TextField("Enter Name (Optional)", text: $contactImageEntry.name)
+                    }
+                ).listRowBackground(Color.chart)
+
                 Section(header: Text("Style")) {
                     Picker("Layout", selection: $contactImageEntry.layout) {
                         ForEach(ContactImageLayout.allCases, id: \.id) { layout in

+ 2 - 0
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -313,6 +313,8 @@ extension DataTable {
                 )
 
                 await syncWithServices()
+                // Perform a determine basal sync to update cob
+                await apsManager.determineBasalSync()
             }
         }
 

+ 1 - 1
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -171,7 +171,7 @@ struct CarbEntryEditorView: View {
                         selection: $editedDate,
                         displayedComponents: [.date, .hourAndMinute]
                     )
-                }
+                }.listRowBackground(Color.chart)
             }
             .safeAreaInset(
                 edge: .bottom,

+ 7 - 3
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -297,11 +297,15 @@ extension DynamicSettings {
                                 }
                                 VStack(alignment: .leading, spacing: 5) {
                                     Text(
-                                        "If your glucose target is 110 mg/dL, Trio will use a safety threshold of 75 mg/dL, unless you set Minimum Safety Threshold (mg/dL) to something > 75."
+                                        "If your glucose target is \(state.units == .mgdL ? "110" : 110.formattedAsMmolL) \(state.units.rawValue), Trio will use a safety threshold of \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue), unless you set Minimum Safety Threshold to something > \(state.units == .mgdL ? "75" : 75.formattedAsMmolL) \(state.units.rawValue)."
+                                    )
+                                    Text(
+                                        "\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - 0.5 × (\(state.units == .mgdL ? "110" : 110.formattedAsMmolL) - \(state.units == .mgdL ? "40" : 40.formattedAsMmolL)) = \(state.units == .mgdL ? "75" : 75.formattedAsMmolL)"
                                     )
-                                    Text("110 - 0.5 × (110 - 40) = 75")
                                 }
-                                Text("This setting is limited to values between 60 - 120 mg/dL (3.3 - 6.6 mmol/L)")
+                                Text(
+                                    "This setting is limited to values between \(state.units == .mgdL ? "60" : 60.formattedAsMmolL) - \(state.units == .mgdL ? "120" : 120.formattedAsMmolL) \(state.units.rawValue)"
+                                )
                                 Text(
                                     "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast."
                                 )

+ 15 - 4
Trio/Sources/Modules/Home/HomeStateModel+Setup/ChartAxisSetup.swift

@@ -19,19 +19,30 @@ extension Home.StateModel {
                 // Ensure all values exist, otherwise set default values
                 guard let minGlucose = minGlucose, let maxGlucose = maxGlucose else {
                     Task {
-                        await self.updateChartBounds(minValue: 39, maxValue: 300)
+                        await self.updateChartBounds(minValue: 39, maxValue: 200)
                     }
                     return
                 }
 
-                // Adjust max forecast to be no more than 100 over max glucose
-                let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 100, maxGlucose + 100)
+                // Adjust max forecast to be no more than 50 over max glucose
+                let adjustedMaxForecast = min(maxForecast ?? maxGlucose + 50, maxGlucose + 50)
                 let minOverall = min(minGlucose, minForecast ?? minGlucose)
                 let maxOverall = max(maxGlucose, adjustedMaxForecast)
 
+                var maxYValue = Decimal(200)
+                if maxOverall > 200, maxOverall <= 225 {
+                    maxYValue = Decimal(250)
+                } else if maxOverall > 225, maxOverall <= 275 {
+                    maxYValue = Decimal(300)
+                } else if maxOverall > 275, maxOverall <= 325 {
+                    maxYValue = Decimal(350)
+                } else if maxOverall > 325 {
+                    maxYValue = Decimal(400)
+                }
+
                 // Update the chart bounds on the main thread
                 Task {
-                    await self.updateChartBounds(minValue: minOverall - 50, maxValue: maxOverall + 80)
+                    await self.updateChartBounds(minValue: minOverall, maxValue: maxYValue)
                 }
             }
         }

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift

@@ -47,7 +47,7 @@ extension Home.StateModel {
             key: "deliverAt",
             ascending: false,
             batchSize: 50,
-            propertiesToFetch: ["cob", "iob", "objectID"]
+            propertiesToFetch: ["cob", "iob", "deliverAt", "objectID"]
         )
 
         return await determinationFetchContext.perform {

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -112,7 +112,7 @@ extension Home {
         var forecastDisplayType: ForecastDisplayType = .cone
 
         var minYAxisValue: Decimal = 39
-        var maxYAxisValue: Decimal = 300
+        var maxYAxisValue: Decimal = 200
 
         var minValueCobChart: Decimal = 0
         var maxValueCobChart: Decimal = 20

+ 18 - 1
Trio/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift

@@ -80,7 +80,24 @@ extension MainChartView {
     }
 
     func drawCOBIOBChart() -> some ChartContent {
-        ForEach(state.enactedAndNonEnactedDeterminations) { item in
+        // Filter out duplicate entries by `deliverAt`,
+        // We sometimes get two determinations when editing carbs, one without the entry-to-be-edited and then another one after editing the entry.
+        // We are fetching determinations in descending order, so the first one is the latter determination (with correct amounts), so keeping the first one encountered.
+        var seenDates = Set<Date>()
+        let filteredDeterminations = state.enactedAndNonEnactedDeterminations.filter { item in
+            if let date = item.deliverAt {
+                if seenDates.contains(date) {
+                    // Already seen this date – filter it out.
+                    return false
+                } else {
+                    seenDates.insert(date)
+                    return true
+                }
+            }
+            return true
+        }
+
+        return ForEach(filteredDeterminations) { item in
 
             // MARK: - COB line and area mark
 

+ 2 - 1
Trio/Sources/Modules/Home/View/Chart/ChartElements/ForecastView.swift

@@ -9,6 +9,7 @@ struct ForecastView: ChartContent {
     let units: GlucoseUnits
     let maxValue: Decimal
     let forecastDisplayType: ForecastDisplayType
+    let lastDeterminationDate: Date
 
     var body: some ChartContent {
         if forecastDisplayType == .lines {
@@ -19,7 +20,7 @@ struct ForecastView: ChartContent {
     }
 
     private func timeForIndex(_ index: Int32) -> Date {
-        let currentTime = Date()
+        let currentTime = lastDeterminationDate
         let timeInterval = TimeInterval(index * 300)
         return currentTime.addingTimeInterval(timeInterval)
     }

+ 2 - 1
Trio/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -167,7 +167,8 @@ extension MainChartView {
                     maxForecast: state.maxForecast,
                     units: state.units,
                     maxValue: state.maxYAxisValue,
-                    forecastDisplayType: state.forecastDisplayType
+                    forecastDisplayType: state.forecastDisplayType,
+                    lastDeterminationDate: state.determinationsFromPersistence.first?.deliverAt ?? .distantPast
                 )
 
                 /// show glucose value when hovering over it

+ 0 - 5
Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -102,11 +102,6 @@ extension ISFEditor {
             .navigationTitle("Insulin Sensitivities")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                         HStack {

+ 0 - 5
Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -86,11 +86,6 @@ extension TargetsEditor {
             .navigationTitle("Glucose Targets")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
-                if state.items.isNotEmpty {
-                    ToolbarItem(placement: .topBarTrailing) {
-                        EditButton()
-                    }
-                }
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: { state.add() }) {
                         HStack {

+ 11 - 13
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -421,19 +421,17 @@ extension Treatments {
                 let isFatPresent = fat > 0
                 let isProteinPresent = protein > 0
 
+                if isCarbsPresent || isFatPresent || isProteinPresent {
+                    await saveMeal()
+                }
+
                 if isInsulinGiven {
                     try await handleInsulin(isExternal: externalInsulin)
-                } else if isCarbsPresent || isFatPresent || isProteinPresent {
-                    await MainActor.run {
-                        self.waitForSuggestion = true
-                    }
                 } else {
                     hideModal()
                     return
                 }
 
-                await saveMeal()
-
                 // If glucose data is stale end the custom loading animation by hiding the modal
                 // Get date on Main thread
                 let date = await MainActor.run {
@@ -548,17 +546,17 @@ extension Treatments {
                     isFPU: false,
                     fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
                 )]
-
                 try await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
 
-                if carbs > 0 || fat > 0 || protein > 0 {
-                    // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
-                    if amount <= 0 {
-                        await apsManager.determineBasalSync()
+                // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
+                if amount <= 0 {
+                    await MainActor.run {
+                        self.waitForSuggestion = true
                     }
+                    await apsManager.determineBasalSync()
                 }
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save meal with error: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save carbs: \(error.localizedDescription)")
             }
         }
 
@@ -823,7 +821,7 @@ extension Treatments.StateModel {
         } else {
             simulatedDetermination = await Task { [self] in
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
-                return await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
+                return await apsManager.simulateDetermineBasal(simulatedCarbsAmount: carbs, simulatedBolusAmount: amount)
             }.value
         }
 

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

@@ -772,9 +772,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                    isf > 0, cr > 0
                 {
                     carbsHr = settingsManager.preferences.min5mCarbimpact * 12 / isf * cr
-                    if settingsManager.settings.units == .mmolL {
-                        carbsHr *= GlucoseUnits.exchangeRate
-                    }
                     carbsHr = Decimal(round(Double(carbsHr) * 10.0)) / 10
                 }
 

+ 27 - 0
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -257,6 +257,31 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 }
                 .sorted { $0.date < $1.date }
 
+                // Set axis domain: min and max Y-axis values
+                // Apply unit parsing conditionally, if user uses mmol/L
+                let maxGlucoseValue = Decimal(glucoseObjects.map { Int($0.glucose) }.max() ?? 200)
+                var maxYValue = Decimal(200)
+
+                if maxGlucoseValue > maxYValue, maxGlucoseValue <= 225 {
+                    maxYValue = Decimal(250)
+                } else if maxGlucoseValue > 225, maxGlucoseValue <= 275 {
+                    maxYValue = Decimal(300)
+                } else if maxGlucoseValue > 275, maxGlucoseValue <= 325 {
+                    maxYValue = Decimal(350)
+                } else if maxGlucoseValue > 325 {
+                    maxYValue = Decimal(400)
+                }
+
+                if self.units == .mmolL {
+                    maxYValue = Double(truncating: maxYValue as NSNumber).asMmolL
+                }
+                watchState.maxYAxisValue = maxYValue
+
+                if self.units == .mmolL {
+                    let minYValue = Double(truncating: watchState.minYAxisValue as NSNumber).asMmolL
+                    watchState.minYAxisValue = minYValue
+                }
+
                 // Convert direction to trend string
                 watchState.trend = latestGlucose.direction
 
@@ -405,6 +430,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     "color": value.color
                 ]
             },
+            WatchMessageKeys.minYAxisValue: state.minYAxisValue,
+            WatchMessageKeys.maxYAxisValue: state.maxYAxisValue,
             WatchMessageKeys.overridePresets: state.overridePresets.map { preset in
                 [
                     "name": preset.name,

+ 1 - 0
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -167,6 +167,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
 
     func cancelTempTarget() async {
         await disableAllActiveTempTargets(createTempTargetRunEntry: true, shouldStartBackgroundTask: true)
+        tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
     }
 
     @MainActor func disableAllActiveTempTargets(