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

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into tempTargets

polscm32 aka Marvout 1 год назад
Родитель
Сommit
b1eca2942e
33 измененных файлов с 735 добавлено и 281 удалено
  1. 78 0
      FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS.xcscheme
  2. 2 2
      FreeAPS/Resources/Info.plist
  3. 7 0
      FreeAPS/Sources/APS/APSManager.swift
  4. 4 0
      FreeAPS/Sources/Logger/Logger.swift
  5. 2 2
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  6. 44 0
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  7. 96 25
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  8. 73 15
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  9. 2 0
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  10. 86 0
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  11. 43 26
      FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  12. 7 8
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  13. 98 31
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  14. 2 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  15. 2 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  16. 2 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  17. 8 10
      FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift
  18. 14 24
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  19. 42 26
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  20. 1 4
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift
  21. 13 9
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift
  22. 4 0
      FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift
  23. 38 21
      FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  24. 2 2
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  25. 3 2
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  26. 1 0
      FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift
  27. 1 1
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  28. 1 1
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  29. 29 46
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  30. 8 4
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  31. 21 17
      Gemfile.lock
  32. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  33. 0 1
      Model/CoreDataStack.swift

+ 78 - 0
FreeAPS.xcodeproj/xcshareddata/xcschemes/FreeAPS.xcscheme

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1600"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "388E595725AD948C0019842D"
+               BuildableName = "FreeAPS.app"
+               BlueprintName = "FreeAPS"
+               ReferencedContainer = "container:FreeAPS.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "388E595725AD948C0019842D"
+            BuildableName = "FreeAPS.app"
+            BlueprintName = "FreeAPS"
+            ReferencedContainer = "container:FreeAPS.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "388E595725AD948C0019842D"
+            BuildableName = "FreeAPS.app"
+            BlueprintName = "FreeAPS"
+            ReferencedContainer = "container:FreeAPS.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 2 - 2
FreeAPS/Resources/Info.plist

@@ -2,8 +2,6 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>TeamID</key>
-	<string>$(DEVELOPER_TEAM)</string>
 	<key>AppGroupID</key>
 	<string>$(APP_GROUP_ID)</string>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
@@ -88,6 +86,8 @@
 	<string>$(COPYRIGHT_NOTICE)</string>
 	<key>NSSupportsLiveActivities</key>
 	<true/>
+	<key>TeamID</key>
+	<string>$(DEVELOPMENT_TEAM)</string>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>

+ 7 - 0
FreeAPS/Sources/APS/APSManager.swift

@@ -273,6 +273,13 @@ final class BaseAPSManager: APSManager, Injectable {
                 await loopCompleted(error: error, loopStatRecord: loopStatRecord)
             }
 
+            if let nightscoutManager = nightscout {
+                await nightscoutManager.uploadCarbs()
+                await nightscoutManager.uploadPumpHistory()
+                await nightscoutManager.uploadOverrides()
+                await nightscoutManager.uploadTempTargets()
+            }
+
             // End background task after all the operations are completed
             if let backgroundTask = self.backGroundTaskID {
                 await UIApplication.shared.endBackgroundTask(backgroundTask)

+ 4 - 0
FreeAPS/Sources/Logger/Logger.swift

@@ -113,6 +113,7 @@ final class Logger {
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
     static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
     static let remoteControl = Logger(category: .remoteControl, reporter: baseReporter)
+    static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
 
     enum Category: String {
         case `default`
@@ -123,6 +124,7 @@ final class Logger {
         case apsManager
         case nightscout
         case remoteControl
+        case bolusState
 
         var name: String {
             rawValue.capitalizingFirstLetter()
@@ -138,6 +140,7 @@ final class Logger {
             case .apsManager: return .apsManager
             case .nightscout: return .nightscout
             case .remoteControl: return .remoteControl
+            case .bolusState: return .bolusState
             }
         }
 
@@ -146,6 +149,7 @@ final class Logger {
             switch self {
             case .default: return OSLog.default
             case .apsManager,
+                 .bolusState,
                  .businessLogic,
                  .deviceManager,
                  .nightscout,

+ 2 - 2
FreeAPS/Sources/Models/DecimalPickerSettings.swift

@@ -33,8 +33,8 @@ class PickerSettingsProvider: ObservableObject {
 }
 
 struct DecimalPickerSettings {
-    var lowGlucose = PickerSetting(value: 72, step: 1, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
-    var highGlucose = PickerSetting(value: 270, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
+    var lowGlucose = PickerSetting(value: 70, step: 5, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
+    var highGlucose = PickerSetting(value: 180, step: 5, min: 100, max: 400, type: PickerSetting.PickerSettingType.glucose)
     var carbsRequiredThreshold = PickerSetting(value: 10, step: 1, min: 0, max: 100, type: PickerSetting.PickerSettingType.gramms)
     var individualAdjustmentFactor = PickerSetting(
         value: 0.5,

+ 44 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -10,6 +10,7 @@ extension BasalProfileEditor {
         var items: [Item] = []
         var total: Decimal = 0.0
         var showAlert: Bool = false
+        var chartData: [BasalProfile]? = []
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
@@ -114,6 +115,49 @@ extension BasalProfileEditor {
                 if self.items != sorted {
                     self.items = sorted
                 }
+                self.calcTotal()
+            }
+        }
+
+        func availableTimeIndices(_ itemIndex: Int) -> [Int] {
+            // avoid index out of range issues
+            guard itemIndex >= 0, itemIndex < items.count else {
+                return []
+            }
+
+            let usedIndicesByOtherItems = items
+                .enumerated()
+                .filter { $0.offset != itemIndex }
+                .map(\.element.timeIndex)
+
+            return (0 ..< timeValues.count).filter { !usedIndicesByOtherItems.contains($0) }
+        }
+
+        func caluclateChartData() {
+            DispatchQueue.main.async {
+                var basals: [BasalProfile] = []
+                let tzOffset = TimeZone.current.secondsFromGMT() * -1
+
+                basals.append(contentsOf: self.items.enumerated().map { index, item in
+                    let startDate = Date(timeIntervalSinceReferenceDate: self.timeValues[item.timeIndex])
+                    var endDate = Date(timeIntervalSinceReferenceDate: self.timeValues.last!).addingTimeInterval(30 * 60)
+                    if self.items.count > index + 1 {
+                        let nextItem = self.items[index + 1]
+                        endDate = Date(timeIntervalSinceReferenceDate: self.timeValues[nextItem.timeIndex])
+                    }
+
+                    return BasalProfile(
+                        amount: Double(self.rateValues[item.rateIndex]),
+                        isOverwritten: false,
+                        startDate: startDate.addingTimeInterval(TimeInterval(tzOffset)),
+                        endDate: endDate.addingTimeInterval(TimeInterval(tzOffset))
+                    )
+                })
+                basals.sort(by: {
+                    $0.startDate > $1.startDate
+                })
+
+                self.chartData = basals
             }
         }
     }

+ 96 - 25
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -1,3 +1,4 @@
+import Charts
 import SwiftUI
 import Swinject
 
@@ -7,6 +8,9 @@ extension BasalProfileEditor {
         @State var state = StateModel()
         @State private var editMode = EditMode.inactive
 
+        let chartScale = Calendar.current
+            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
             colorScheme == .dark ? LinearGradient(
@@ -38,11 +42,92 @@ extension BasalProfileEditor {
             return formatter
         }
 
-        var body: some View {
-            Form {
+        var basalScheduleChart: some View {
+            Chart {
+                ForEach(state.chartData!, id: \.self) { profile in
+                    RectangleMark(
+                        xStart: .value("start", profile.startDate),
+                        xEnd: .value("end", profile.endDate!),
+                        yStart: .value("rate-start", profile.amount),
+                        yEnd: .value("rate-end", 0)
+                    ).foregroundStyle(
+                        .linearGradient(
+                            colors: [
+                                Color.insulin.opacity(0.6),
+                                Color.insulin.opacity(0.1)
+                            ],
+                            startPoint: .bottom,
+                            endPoint: .top
+                        )
+                    ).alignsMarkStylesWithPlotArea()
+
+                    LineMark(x: .value("End Date", profile.endDate!), y: .value("Amount", profile.amount))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+
+                    LineMark(x: .value("Start Date", profile.startDate), y: .value("Amount", profile.amount))
+                        .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
+                }
+            }
+            .chartXAxis {
+                AxisMarks(values: .automatic(desiredCount: 6)) { _ in
+                    AxisValueLabel(format: .dateTime.hour())
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartYAxis {
+                AxisMarks(values: .automatic(desiredCount: 2)) { _ in
+                    AxisValueLabel()
+                    AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
+                }
+            }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
+        }
+
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.syncInProgress || state.items.isEmpty || !state.hasChanges
 
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Group {
+                    HStack {
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+                        } label: {
+                            HStack {
+                                if state.syncInProgress {
+                                    ProgressView().padding(.trailing, 10)
+                                }
+                                Text(state.syncInProgress ? "Saving..." : "Save")
+                            }.padding(10)
+                        }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                        .disabled(shouldDisableButton)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                        .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
                 Section(header: Text("Schedule")) {
+                    if !state.items.isEmpty {
+                        basalScheduleChart.padding(.vertical)
+                    }
+
                     list
                 }.listRowBackground(Color.chart)
 
@@ -58,25 +143,8 @@ extension BasalProfileEditor {
                             .foregroundColor(.secondary)
                     }
                 }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        if state.syncInProgress {
-                            ProgressView().padding(.trailing, 10)
-                        }
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
-                        } label: {
-                            Text(state.syncInProgress ? "Saving..." : "Save")
-                        }
-                        .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .alert(isPresented: $state.showAlert) {
                 Alert(
                     title: Text("Unable to Save"),
@@ -84,11 +152,11 @@ extension BasalProfileEditor {
                     dismissButton: .default(Text("Close"))
                 )
             }
-            .onChange(of: state.items) { _ in
+            .onChange(of: state.items) {
                 state.calcTotal()
+                state.caluclateChartData()
             }
             .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
             .navigationTitle("Basal Profile")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
@@ -101,7 +169,9 @@ extension BasalProfileEditor {
             })
             .environment(\.editMode, $editMode)
             .onAppear {
+                configureView()
                 state.validate()
+                state.caluclateChartData()
             }
         }
 
@@ -118,12 +188,12 @@ extension BasalProfileEditor {
                             ).tag(i)
                         }
                     }
-                    .onChange(of: state.items[index].rateIndex, perform: { _ in state.calcTotal() })
+                    .onChange(of: state.items[index].rateIndex, { state.calcTotal() })
                 }.listRowBackground(Color.chart)
 
                 Section {
                     Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
+                        ForEach(state.availableTimeIndices(index), id: \.self) { i in
                             Text(
                                 self.dateFormatter
                                     .string(from: Date(
@@ -133,7 +203,7 @@ extension BasalProfileEditor {
                             ).tag(i)
                         }
                     }
-                    .onChange(of: state.items[index].timeIndex, perform: { _ in state.calcTotal() })
+                    .onChange(of: state.items[index].timeIndex, { state.calcTotal() })
                 }.listRowBackground(Color.chart)
             }
             .padding(.top)
@@ -185,6 +255,7 @@ extension BasalProfileEditor {
             state.items.remove(atOffsets: offsets)
             state.validate()
             state.calcTotal()
+            state.caluclateChartData()
         }
     }
 }

+ 73 - 15
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -119,12 +119,24 @@ extension Bolus {
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
 
+        var isActive: Bool = false
+
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
         private var subscriptions = Set<AnyCancellable>()
 
         typealias PumpEvent = PumpEventStored.EventType
 
+        func unsubscribe() {
+            subscriptions.forEach { $0.cancel() }
+            subscriptions.removeAll()
+        }
+
         override func subscribe() {
+            guard isActive else {
+                return
+            }
+
+            debug(.bolusState, "subscribe fired")
             coreDataPublisher =
                 changedObjectsOnManagedObjectContextDidSavePublisher()
                     .receive(on: DispatchQueue.global(qos: .background))
@@ -135,7 +147,19 @@ extension Bolus {
             setupBolusStateConcurrently()
         }
 
+        deinit {
+            // Unregister from broadcaster
+            broadcaster.unregister(DeterminationObserver.self, observer: self)
+            broadcaster.unregister(BolusFailureObserver.self, observer: self)
+
+            // Cancel Combine subscriptions
+            unsubscribe()
+
+            debug(.bolusState, "Bolus.StateModel deinitialized")
+        }
+
         private func setupBolusStateConcurrently() {
+            debug(.bolusState, "setupBolusStateConcurrently fired")
             Task {
                 await withTaskGroup(of: Void.self) { group in
                     group.addTask {
@@ -241,9 +265,10 @@ extension Bolus {
             let now = Date()
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm"
             dateFormatter.timeZone = TimeZone.current
 
+            let regexWithSeconds = #"^\d{2}:\d{2}:\d{2}$"#
+
             let entries: [(start: String, value: Decimal)]
 
             switch type {
@@ -262,6 +287,13 @@ extension Bolus {
             }
 
             for (index, entry) in entries.enumerated() {
+                // Dynamically set the format based on whether it matches the regex
+                if entry.start.range(of: regexWithSeconds, options: .regularExpression) != nil {
+                    dateFormatter.dateFormat = "HH:mm:ss"
+                } else {
+                    dateFormatter.dateFormat = "HH:mm"
+                }
+
                 guard let entryTime = dateFormatter.date(from: entry.start) else {
                     print("Invalid entry start time: \(entry.start)")
                     continue
@@ -271,21 +303,30 @@ extension Bolus {
                 let entryStartTime = calendar.date(
                     bySettingHour: entryComponents.hour!,
                     minute: entryComponents.minute!,
-                    second: entryComponents.second!,
+                    second: entryComponents.second ?? 0, // Set seconds to 0 if not provided
                     of: now
                 )!
 
                 let entryEndTime: Date
-                if index < entries.count - 1,
-                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
-                {
-                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
-                    entryEndTime = calendar.date(
-                        bySettingHour: nextEntryComponents.hour!,
-                        minute: nextEntryComponents.minute!,
-                        second: nextEntryComponents.second!,
-                        of: now
-                    )!
+                if index < entries.count - 1 {
+                    // Dynamically set the format again for the next element
+                    if entries[index + 1].start.range(of: regexWithSeconds, options: .regularExpression) != nil {
+                        dateFormatter.dateFormat = "HH:mm:ss"
+                    } else {
+                        dateFormatter.dateFormat = "HH:mm"
+                    }
+
+                    if let nextEntryTime = dateFormatter.date(from: entries[index + 1].start) {
+                        let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                        entryEndTime = calendar.date(
+                            bySettingHour: nextEntryComponents.hour!,
+                            minute: nextEntryComponents.minute!,
+                            second: nextEntryComponents.second ?? 0,
+                            of: now
+                        )!
+                    } else {
+                        entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+                    }
                 } else {
                     entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
                 }
@@ -312,6 +353,7 @@ extension Bolus {
 
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
+            debug(.bolusState, "calculateInsulin fired")
             let isfForCalculation = isf
 
             // insulin needed for the current blood glucose
@@ -359,7 +401,6 @@ extension Bolus {
             insulinCalculated = min(insulinCalculated, maxBolus)
 
             guard let apsManager = apsManager else {
-                debug(.apsManager, "APSManager could not be gracefully unwrapped")
                 return insulinCalculated
             }
 
@@ -370,6 +411,7 @@ extension Bolus {
 
         func invokeTreatmentsTask() {
             Task {
+                debug(.bolusState, "invokeTreatmentsTask fired")
                 await MainActor.run {
                     self.addButtonPressed = true
                 }
@@ -409,6 +451,7 @@ extension Bolus {
         // MARK: - Insulin
 
         private func handleInsulin(isExternal: Bool) async throws {
+            debug(.bolusState, "handleInsulin fired")
             if !isExternal {
                 await addPumpInsulin()
             } else {
@@ -557,7 +600,13 @@ extension Bolus {
 
 extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
     func determinationDidUpdate(_: Determination) {
+        guard isActive else {
+            debug(.bolusState, "skipping determinationDidUpdate; view not active")
+            return
+        }
+
         DispatchQueue.main.async {
+            debug(.bolusState, "determinationDidUpdate fired")
             self.waitForSuggestion = false
             if self.addButtonPressed {
                 self.hideModal()
@@ -567,6 +616,7 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 
     func bolusDidFail() {
         DispatchQueue.main.async {
+            debug(.bolusState, "bolusDidFail fired")
             self.waitForSuggestion = false
             if self.addButtonPressed {
                 self.hideModal()
@@ -581,7 +631,8 @@ extension Bolus.StateModel {
             guard let self = self else { return }
             Task {
                 await self.setupDeterminationsArray()
-                await self.updateForecasts()
+                let forecastData = await self.mapForecastsForChart()
+                await self.updateForecasts(with: forecastData)
             }
         }.store(in: &subscriptions)
 
@@ -733,11 +784,18 @@ extension Bolus.StateModel {
 
 extension Bolus.StateModel {
     @MainActor func updateForecasts(with forecastData: Determination? = nil) async {
+        guard isActive else {
+            return
+                debug(.bolusState, "updateForecasts not fired")
+        }
+
+        debug(.bolusState, "updateForecasts fired")
         if let forecastData = forecastData {
             simulatedDetermination = forecastData
         } else {
             simulatedDetermination = await Task { [self] in
-                await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
+                debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
+                return await apsManager.simulateDetermineBasal(carbs: carbs, iob: amount)
             }.value
         }
 

+ 2 - 0
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -344,10 +344,12 @@ extension Bolus {
             })
             .onAppear {
                 configureView {
+                    state.isActive = true
                     state.insulinCalculated = state.calculateInsulin()
                 }
             }
             .onDisappear {
+                state.isActive = false
                 state.addButtonPressed = false
             }
             .sheet(isPresented: $state.showInfo) {

+ 86 - 0
FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift

@@ -9,6 +9,8 @@ struct ForecastChart: View {
 
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
 
+    @State var selection: Date? = nil
+
     private var endMarker: Date {
         state
             .forecastDisplayType == .lines ? Date(timeIntervalSinceNow: TimeInterval(hours: 3)) :
@@ -32,6 +34,12 @@ struct ForecastChart: View {
         return formatter
     }
 
+    private var selectedGlucose: GlucoseStored? {
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
+        return state.glucoseFromPersistence.first { $0.date.map(range.contains) ?? false }
+    }
+
     var body: some View {
         VStack {
             forecastChartLabels
@@ -114,7 +122,43 @@ struct ForecastChart: View {
             } else {
                 drawForecastsCone()
             }
+
+            if let selectedGlucose {
+                RuleMark(x: .value("Selection", selectedGlucose.date ?? Date.now, unit: .minute))
+                    .foregroundStyle(Color.tabBar)
+                    .lineStyle(.init(lineWidth: 2))
+                    .annotation(
+                        position: .top,
+                        overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
+                    ) {
+                        selectionPopover
+                    }
+
+                PointMark(
+                    x: .value("Time", selectedGlucose.date ?? Date.now, unit: .minute),
+                    y: .value("Value", selectedGlucose.glucose)
+                )
+                .zIndex(-1)
+                .symbolSize(CGSize(width: 15, height: 15))
+                .foregroundStyle(
+                    Decimal(selectedGlucose.glucose) > state.highGlucose ? Color.orange
+                        .opacity(0.8) :
+                        (
+                            Decimal(selectedGlucose.glucose) < state.lowGlucose ? Color.red.opacity(0.8) : Color.green
+                                .opacity(0.8)
+                        )
+                )
+
+                PointMark(
+                    x: .value("Time", selectedGlucose.date ?? Date.now, unit: .minute),
+                    y: .value("Value", selectedGlucose.glucose)
+                )
+                .zIndex(-1)
+                .symbolSize(CGSize(width: 6, height: 6))
+                .foregroundStyle(Color.primary)
+            }
         }
+        .chartXSelection(value: $selection)
         .chartXAxis { forecastChartXAxis }
         .chartXScale(domain: startMarker ... endMarker)
         .chartYAxis { forecastChartYAxis }
@@ -122,6 +166,48 @@ struct ForecastChart: View {
         .backport.chartForegroundStyleScale(state: state)
     }
 
+    @ViewBuilder var selectionPopover: some View {
+        if let sgv = selectedGlucose?.glucose {
+            VStack(alignment: .leading) {
+                HStack {
+                    Image(systemName: "clock")
+                    Text(selectedGlucose?.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
+                        .font(.footnote).bold()
+                }.font(.footnote).padding(.bottom, 5)
+
+                // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                let hardCodedLow = Decimal(55)
+                let hardCodedHigh = Decimal(220)
+                let isDynamicColorScheme = state.glucoseColorScheme == .dynamicColor
+
+                let glucoseColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(sgv),
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : state.highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : state.lowGlucose,
+                    targetGlucose: state.currentBGTarget,
+                    glucoseColorScheme: state.glucoseColorScheme
+                )
+                HStack {
+                    Text(state.units == .mgdL ? Decimal(sgv).description : Decimal(sgv).formattedAsMmolL)
+                        .bold()
+                        + Text(" \(state.units.rawValue)")
+                }.foregroundStyle(
+                    Color(glucoseColor)
+                ).font(.footnote)
+            }
+            .padding(7)
+            .background {
+                RoundedRectangle(cornerRadius: 4)
+                    .fill(Color.chart.opacity(0.85))
+                    .shadow(color: Color.secondary, radius: 2)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 4)
+                            .stroke(Color.secondary, lineWidth: 2)
+                    )
+            }
+        }
+    }
+
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL

+ 43 - 26
FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -38,10 +38,50 @@ extension CarbRatioEditor {
             return formatter
         }
 
-        var body: some View {
-            Form {
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
 
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Group {
+                    HStack {
+                        HStack {
+                            Button {
+                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                                impactHeavy.impactOccurred()
+                                state.save()
+
+                                // deactivate saving display after 1.25 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                    state.shouldDisplaySaving = false
+                                }
+                            } label: {
+                                HStack {
+                                    if state.shouldDisplaySaving {
+                                        ProgressView().padding(.trailing, 10)
+                                    }
+                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                                }.padding(10)
+                            }
+                        }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                        .disabled(shouldDisableButton)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                        .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                         HStack {
@@ -56,31 +96,8 @@ extension CarbRatioEditor {
                 Section(header: Text("Schedule")) {
                     list
                 }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        if state.shouldDisplaySaving {
-                            ProgressView().padding(.trailing, 10)
-                        }
-
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
-
-                            // deactivate saving display after 1.25 seconds
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                state.shouldDisplaySaving = false
-                            }
-                        } label: {
-                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                        }
-                        .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .navigationTitle("Carb Ratios")

+ 7 - 8
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -20,20 +20,19 @@ extension GlucoseNotificationSettings {
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
                 addSourceInfoToGlucoseNotifications = $0 }
+
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
-                let value = max(min($0, 400), 40)
-                lowGlucose = value
+                lowGlucose = $0
             }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
+                let clampedValue = max(min($0, 400), 40)
+                return clampedValue
             })
 
             subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
-                let value = max(min($0, 400), 40)
-                highGlucose = value
+                highGlucose = $0
             }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
+                let clampedValue = max(min($0, 400), 40)
+                return clampedValue
             })
         }
     }

+ 98 - 31
FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -14,6 +14,8 @@ extension GlucoseNotificationSettings {
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
+        @State private var displayPickerLowGlucose: Bool = false
+        @State private var displayPickerHighGlucose: Bool = false
 
         private var glucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -127,34 +129,113 @@ extension GlucoseNotificationSettings {
                     verboseHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
                 )
 
-                Section {
-                    HStack {
-                        Text("Low Glucose Alarm Limit")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.lowGlucose, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }.padding(.top)
+                self.lowAndHighGlucoseAlertSection
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Glucose Notifications")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
 
-                    HStack {
-                        Text("High Glucose Alarm Limit")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.highGlucose, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
+        var lowAndHighGlucoseAlertSection: some View {
+            Section {
+                VStack {
+                    VStack {
+                        HStack {
+                            Text("Low Glucose Alarm Limit")
+
+                            Spacer()
+
+                            Group {
+                                Text(
+                                    state.units == .mgdL ? state.lowGlucose.description : state.lowGlucose.formattedAsMmolL
+                                )
+                                .foregroundColor(!displayPickerLowGlucose ? .primary : .accentColor)
+
+                                Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
+                            }
+                        }
+                        .onTapGesture {
+                            displayPickerLowGlucose.toggle()
+                        }
+                    }
+                    .padding(.top)
+
+                    if displayPickerLowGlucose {
+                        let setting = PickerSettingsProvider.shared.settings.lowGlucose
+
+                        Picker(selection: $state.lowGlucose, label: Text("")) {
+                            ForEach(
+                                PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
+                                id: \.self
+                            ) { value in
+                                let displayValue = state.units == .mgdL ? value.description : value.formattedAsMmolL
+                                Text(displayValue).tag(value)
+                            }
+                        }
+                        .pickerStyle(WheelPickerStyle())
+                        .frame(maxWidth: .infinity)
+                    }
+
+                    VStack {
+                        HStack {
+                            Text("High Glucose Alarm Limit")
+
+                            Spacer()
+
+                            Group {
+                                Text(
+                                    state.units == .mgdL ? state.highGlucose.description : state.highGlucose.formattedAsMmolL
+                                )
+                                .foregroundColor(!displayPickerHighGlucose ? .primary : .accentColor)
+
+                                Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
+                            }
+                        }
+                        .onTapGesture {
+                            displayPickerHighGlucose.toggle()
+                        }
+                    }
+                    .padding(.top)
+
+                    if displayPickerHighGlucose {
+                        let setting = PickerSettingsProvider.shared.settings.highGlucose
+                        Picker(selection: $state.highGlucose, label: Text("")) {
+                            ForEach(
+                                PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
+                                id: \.self
+                            ) { value in
+                                let displayValue = state.units == .mgdL ? value.description : value.formattedAsMmolL
+                                Text(displayValue).tag(value)
+                            }
+                        }
+                        .pickerStyle(WheelPickerStyle())
+                        .frame(maxWidth: .infinity)
                     }
 
                     HStack(alignment: .top) {
                         Text(
                             "Set the lower and upper limit for glucose alarms. See hint for more details."
                         )
+                        .lineLimit(nil)
                         .font(.footnote)
                         .foregroundColor(.secondary)
-                        .lineLimit(nil)
+
                         Spacer()
                         Button(
                             action: {
                                 hintLabel = "Low and High Glucose Alarm Limits"
                                 selectedVerboseHint =
-                                    "These two settings limit the range outside of which you will be notified via push notifications. If your CGM readings are below 'Low' or above 'High', you will receive a glucose alarm."
+                                    "These two settings limit the range outside of which you will be notified via push notifications. If your CGM readings are below 'Low' or above 'High', you will receive an alarm via push notification."
                                 shouldDisplayHint.toggle()
                             },
                             label: {
@@ -163,23 +244,9 @@ extension GlucoseNotificationSettings {
                                 }
                             }
                         ).buttonStyle(BorderlessButtonStyle())
-                    }.padding(.vertical)
-                }
-                .listRowBackground(Color.chart)
-            }
-            .sheet(isPresented: $shouldDisplayHint) {
-                SettingInputHintView(
-                    hintDetent: $hintDetent,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    hintLabel: hintLabel ?? "",
-                    hintText: selectedVerboseHint ?? "",
-                    sheetTitle: "Help"
-                )
-            }
-            .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
-            .navigationBarTitle("Glucose Notifications")
-            .navigationBarTitleDisplayMode(.automatic)
+                    }.padding(.top)
+                }.padding(.bottom)
+            }.listRowBackground(Color.chart)
         }
     }
 }

+ 2 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift

@@ -16,7 +16,8 @@ extension Home.StateModel {
             onContext: carbsFetchContext,
             predicate: NSPredicate.carbsForChart,
             key: "date",
-            ascending: false
+            ascending: false,
+            batchSize: 5
         )
 
         return await carbsFetchContext.perform {

+ 2 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -16,7 +16,8 @@ extension Home.StateModel {
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             key: "date",
-            ascending: true
+            ascending: true,
+            batchSize: 50
         )
 
         return await glucoseFetchContext.perform {

+ 2 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -16,7 +16,8 @@ extension Home.StateModel {
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.pumpHistoryLast24h,
             key: "timestamp",
-            ascending: true
+            ascending: true,
+            batchSize: 30
         )
 
         return await pumpHistoryFetchContext.perform {

+ 8 - 10
FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift

@@ -68,17 +68,15 @@ extension MainChartView {
                     yStart: .value("rate-start", 0),
                     yEnd: .value("rate-end", basal.rate)
                 ).foregroundStyle(
-                    LinearGradient(
-                        gradient: Gradient(
-                            colors: [
-                                Color.insulin.opacity(0.6),
-                                Color.insulin.opacity(0.1)
-                            ]
-                        ),
-                        startPoint: .top,
-                        endPoint: .bottom
+                    .linearGradient(
+                        colors: [
+                            Color.insulin.opacity(0.6),
+                            Color.insulin.opacity(0.1)
+                        ],
+                        startPoint: .bottom,
+                        endPoint: .top
                     )
-                )
+                ).alignsMarkStylesWithPlotArea()
 
                 LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
                     .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)

+ 14 - 24
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -42,37 +42,27 @@ struct MainChartView: View {
     }
 
     private var selectedGlucose: GlucoseStored? {
-        if let selection = selection {
-            let lowerBound = selection.addingTimeInterval(-150)
-            let upperBound = selection.addingTimeInterval(150)
-            return state.glucoseFromPersistence.first { $0.date ?? now >= lowerBound && $0.date ?? now <= upperBound }
-        } else {
-            return nil
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
+        return state.glucoseFromPersistence.first { $0.date.map(range.contains) ?? false }
+    }
+
+    private func findDetermination(in range: ClosedRange<Date>) -> OrefDetermination? {
+        state.enactedAndNonEnactedDeterminations.first {
+            $0.deliverAt ?? now >= range.lowerBound && $0.deliverAt ?? now <= range.upperBound
         }
     }
 
     var selectedCOBValue: OrefDetermination? {
-        if let selection = selection {
-            let lowerBound = selection.addingTimeInterval(-120)
-            let upperBound = selection.addingTimeInterval(120)
-            return state.enactedAndNonEnactedDeterminations.first {
-                $0.deliverAt ?? now >= lowerBound && $0.deliverAt ?? now <= upperBound
-            }
-        } else {
-            return nil
-        }
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-120) ... selection.addingTimeInterval(120)
+        return findDetermination(in: range)
     }
 
     var selectedIOBValue: OrefDetermination? {
-        if let selection = selection {
-            let lowerBound = selection.addingTimeInterval(-120)
-            let upperBound = selection.addingTimeInterval(120)
-            return state.enactedAndNonEnactedDeterminations.first {
-                $0.deliverAt ?? now >= lowerBound && $0.deliverAt ?? now <= upperBound
-            }
-        } else {
-            return nil
-        }
+        guard let selection = selection else { return nil }
+        let range = selection.addingTimeInterval(-120) ... selection.addingTimeInterval(120)
+        return findDetermination(in: range)
     }
 
     var body: some View {

+ 42 - 26
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -39,10 +39,49 @@ extension ISFEditor {
             return formatter
         }
 
-        var body: some View {
-            Form {
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.items.isEmpty || !state.hasChanges
 
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+
+                Group {
+                    HStack {
+                        HStack {
+                            if state.shouldDisplaySaving {
+                                ProgressView().padding(.trailing, 10)
+                            }
+
+                            Button {
+                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                                impactHeavy.impactOccurred()
+                                state.save()
+
+                                // deactivate saving display after 1.25 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                    state.shouldDisplaySaving = false
+                                }
+                            } label: {
+                                Text(state.shouldDisplaySaving ? "Saving..." : "Save").padding(10)
+                            }
+                        }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                        .disabled(shouldDisableButton)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                        .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                         HStack {
@@ -99,31 +138,8 @@ extension ISFEditor {
                 Section(header: Text("Schedule")) {
                     list
                 }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        if state.shouldDisplaySaving {
-                            ProgressView().padding(.trailing, 10)
-                        }
-
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
-
-                            // deactivate saving display after 1.25 seconds
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                state.shouldDisplaySaving = false
-                            }
-                        } label: {
-                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                        }
-                        .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .tint(.white)
-                    }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .navigationTitle("Insulin Sensitivities")

+ 1 - 4
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+APNS.swift

@@ -28,9 +28,6 @@ extension TrioRemoteControl {
     }
 
     private func isRunningInAPNSProductionEnvironment() -> Bool {
-        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL {
-            return appStoreReceiptURL.lastPathComponent != "sandboxReceipt"
-        }
-        return false
+        return BuildDetails.default.isTestFlightBuild()
     }
 }

+ 13 - 9
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl+Override.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 
 extension TrioRemoteControl {
@@ -30,12 +31,12 @@ extension TrioRemoteControl {
     }
 
     @MainActor private func enactOverridePreset(preset: OverrideStored, pushMessage: PushMessage) async {
-        await disableAllActiveOverrides()
-
         preset.enabled = true
         preset.date = Date()
         preset.isUploadedToNS = false
 
+        await disableAllActiveOverrides(except: preset.objectID)
+
         do {
             if viewContext.hasChanges {
                 try viewContext.save()
@@ -43,17 +44,14 @@ extension TrioRemoteControl {
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 await awaitNotification(.didUpdateOverrideConfiguration)
 
-                debug(
-                    .remoteControl,
-                    "Remote command processed successfully. \(pushMessage.humanReadableDescription())"
-                )
+                debug(.remoteControl, "Remote command processed successfully. \(pushMessage.humanReadableDescription())")
             }
         } catch {
             debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
         }
     }
 
-    @MainActor private func disableAllActiveOverrides() async {
+    @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
         let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
         let didPostNotification = await viewContext.perform { () -> Bool in
@@ -65,17 +63,23 @@ extension TrioRemoteControl {
                 guard !results.isEmpty else { return false }
 
                 for canceledOverride in results where canceledOverride.enabled {
+                    if let overrideID = overrideID, canceledOverride.objectID == overrideID {
+                        continue
+                    }
+
                     let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
                     newOverrideRunStored.id = UUID()
                     newOverrideRunStored.name = canceledOverride.name
                     newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                     newOverrideRunStored.endDate = Date()
-                    newOverrideRunStored
-                        .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                    newOverrideRunStored.target = NSDecimalNumber(
+                        decimal: self.overrideStorage.calculateTarget(override: canceledOverride)
+                    )
                     newOverrideRunStored.override = canceledOverride
                     newOverrideRunStored.isUploadedToNS = false
 
                     canceledOverride.enabled = false
+                    canceledOverride.isUploadedToNS = false
                 }
 
                 if self.viewContext.hasChanges {

+ 4 - 0
FreeAPS/Sources/Modules/RemoteControl/TrioRemoteControl.swift

@@ -74,6 +74,10 @@ class TrioRemoteControl: Injectable {
             await cancelTempTarget(pushMessage)
         case .meal:
             await handleMealCommand(pushMessage)
+
+            if pushMessage.bolusAmount != nil {
+                await handleBolusCommand(pushMessage)
+            }
         case .startOverride:
             await handleStartOverrideCommand(pushMessage)
         case .cancelOverride:

+ 38 - 21
FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -32,38 +32,55 @@ extension TargetsEditor {
             return formatter
         }
 
-        var body: some View {
-            Form {
+        var saveButton: some View {
+            ZStack {
                 let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
 
-                Section(header: Text("Schedule")) {
-                    list
-                }.listRowBackground(Color.chart)
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
 
-                Section {
+                Group {
                     HStack {
-                        if state.shouldDisplaySaving {
-                            ProgressView().padding(.trailing, 10)
-                        }
-
-                        Button {
-                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                            impactHeavy.impactOccurred()
-                            state.save()
+                        HStack {
+                            Button {
+                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                                impactHeavy.impactOccurred()
+                                state.save()
 
-                            // deactivate saving display after 1.25 seconds
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                state.shouldDisplaySaving = false
+                                // deactivate saving display after 1.25 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                    state.shouldDisplaySaving = false
+                                }
+                            } label: {
+                                HStack {
+                                    if state.shouldDisplaySaving {
+                                        ProgressView().padding(.trailing, 10)
+                                    }
+                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                                }.padding(10)
                             }
-                        } label: {
-                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
                         }
+                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
                         .disabled(shouldDisableButton)
-                        .frame(maxWidth: .infinity, alignment: .center)
+                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
                         .tint(.white)
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
                     }
-                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                }.padding(5)
+            }
+        }
+
+        var body: some View {
+            Form {
+                Section(header: Text("Schedule")) {
+                    list
+                }.listRowBackground(Color.chart)
             }
+            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .navigationTitle("Target Glucose")

+ 2 - 2
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -258,8 +258,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
 
             let directionText = lastGlucoseObject.directionEnum?.symbol ?? "↔︎"
 
-            let deltaValue = settingsManager.settings.units == .mmolL ? Int(delta.asMmolL) : Int(delta)
-            let deltaText = deltaFormatter.string(from: NSNumber(value: deltaValue)) ?? "--"
+            let deltaValue = settingsManager.settings.units == .mmolL ? delta.asMmolL : delta
+            let deltaText = deltaFormatter.string(from: deltaValue as NSNumber) ?? "--"
 
             let iobText = iobFormatter.string(from: (determinationObject.iob ?? 0) as NSNumber) ?? ""
             let cobText = cobFormatter.string(from: determinationObject.cob as NSNumber) ?? ""

+ 3 - 2
FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -33,7 +33,7 @@ extension LiveActivityBridge {
             key: "deliverAt",
             ascending: false,
             fetchLimit: 1,
-            propertiesToFetch: ["iob", "cob", "currentTarget"]
+            propertiesToFetch: ["iob", "cob", "currentTarget", "deliverAt"]
         )
 
         return await context.perform {
@@ -45,7 +45,8 @@ extension LiveActivityBridge {
                 DeterminationData(
                     cob: ($0["cob"] as? Int) ?? 0,
                     iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0,
-                    target: ($0["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0
+                    target: ($0["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0,
+                    date: $0["deliverAt"] as? Date ?? nil
                 )
             }
         }

+ 1 - 0
FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift

@@ -4,4 +4,5 @@ struct DeterminationData {
     let cob: Int
     let iob: Decimal
     let target: Decimal
+    let date: Date?
 }

+ 1 - 1
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -16,7 +16,7 @@ struct LiveActivityAttributes: ActivityAttributes {
         let bg: String
         let direction: String?
         let change: String
-        let date: Date
+        let date: Date?
         let highGlucose: Decimal
         let lowGlucose: Decimal
         let target: Decimal

+ 1 - 1
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -126,7 +126,7 @@ extension LiveActivityAttributes.ContentState {
             bg: formattedBG,
             direction: trendString,
             change: change,
-            date: bg.date,
+            date: determination?.date ?? nil,
             highGlucose: settings.high,
             lowGlucose: settings.low,
             target: determination?.target ?? 100 as Decimal,

+ 29 - 46
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -15,18 +15,17 @@ import UIKit
              .ended,
              .stale:
             return true
-        case .active: break
+        case .active:
+            break
         @unknown default:
             return true
         }
-
-        return -startDate.timeIntervalSinceNow >
-            TimeInterval(60 * 60)
+        return -startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
     }
 }
 
-@available(iOS 16.2, *) final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver
-{
+@available(iOS 16.2, *)
+final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
@@ -90,8 +89,6 @@ import UIKit
         )
     }
 
-    // TODO: - use a delegate or a custom notification here instead
-
     func settingsDidChange(_: FreeAPSSettings) {
         Task {
             await updateContentState(determination)
@@ -99,7 +96,6 @@ import UIKit
     }
 
     private func registerHandler() {
-        // Since we are only using this info to show if an Override is active or not in the Live Activity it is enough to observe only the 'OverrideStored' Entity
         coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             self.overridesDidUpdate()
@@ -141,15 +137,16 @@ import UIKit
 
     @objc private func handleLiveActivityOrderChange() {
         Task {
-            self.widgetItems = UserDefaults.standard
-                .loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes.LiveActivityItem.defaultItems
+            self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
+                .LiveActivityItem.defaultItems
             await self.updateLiveActivityOrder()
         }
     }
 
     @MainActor private func updateContentState<T>(_ update: T) async {
-        guard let latestGlucose = latestGlucose else { return }
-
+        guard let latestGlucose = latestGlucose else {
+            return
+        }
         var content: LiveActivityAttributes.ContentState?
 
         if let determination = update as? DeterminationData {
@@ -189,10 +186,7 @@ import UIKit
 
     private func setupGlucoseArray() {
         Task { @MainActor in
-            // Fetch and map glucose to GlucoseData struct
             self.glucoseFromPersistence = await fetchAndMapGlucose()
-
-            // Push the update to the Live Activity
             glucoseDidUpdate(glucoseFromPersistence ?? [])
         }
     }
@@ -209,14 +203,9 @@ import UIKit
         }
     }
 
-    /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
-    /// Ends existing live activities if live activities are not enabled in settings
     @MainActor private func forceActivityUpdate() {
-        // just before app resigns active, show a new activity
-        // only do this if there is no current activity or the current activity is older than 1h
         if settings.useLiveActivity {
-            if currentActivity?.needsRecreation() ?? true
-            {
+            if currentActivity?.needsRecreation() ?? true {
                 glucoseDidUpdate(glucoseFromPersistence ?? [])
             }
         } else {
@@ -226,9 +215,7 @@ import UIKit
         }
     }
 
-    /// attempts to present this live activity state, creating a new activity if none exists yet
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
-//        // End all activities that are not the current one
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
         {
@@ -242,32 +229,29 @@ import UIKit
             } else {
                 let content = ActivityContent(
                     state: state,
-                    staleDate: min(state.date, Date.now).addingTimeInterval(360) // 6 minutes in seconds
+                    staleDate: min(state.date ?? Date.now, Date.now).addingTimeInterval(360) // 6 minutes in seconds
                 )
                 await currentActivity.activity.update(content)
             }
         } else {
             do {
-                // always push a non-stale content as the first update
-                // pushing a stale content as the frst content results in the activity not being shown at all
-                // apparently this initial state is also what is shown after the live activity expires (after 8h)
                 let expired = ActivityContent(
-                    state: LiveActivityAttributes.ContentState(
-                        bg: "--",
-                        direction: nil,
-                        change: "--",
-                        date: Date.now,
-                        highGlucose: settings.high,
-                        lowGlucose: settings.low,
-                        target: determination?.target ?? 100 as Decimal,
-                        glucoseColorScheme: settings.glucoseColorScheme.rawValue,
-                        detailedViewState: nil,
-                        isInitialState: true
-                    ),
+                    state: LiveActivityAttributes
+                        .ContentState(
+                            bg: "--",
+                            direction: nil,
+                            change: "--",
+                            date: Date.now,
+                            highGlucose: settings.high,
+                            lowGlucose: settings.low,
+                            target: determination?.target ?? 100 as Decimal,
+                            glucoseColorScheme: settings.glucoseColorScheme.rawValue,
+                            detailedViewState: nil,
+                            isInitialState: true
+                        ),
                     staleDate: Date.now.addingTimeInterval(60)
                 )
 
-                // Request a new activity
                 let activity = try Activity.request(
                     attributes: LiveActivityAttributes(startDate: Date.now),
                     content: expired,
@@ -275,22 +259,22 @@ import UIKit
                 )
                 currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
 
-                // then show the actual content
                 await pushUpdate(state)
             } catch {
-                print("Activity creation error: \(error)")
+                debug(
+                    .default,
+                    "\(#file): Error creating new activity: \(error)"
+                )
             }
         }
     }
 
-    /// ends all live activities immediateny
     private func endActivity() async {
         if let currentActivity {
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             self.currentActivity = nil
         }
 
-        // end any other activities
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
@@ -309,7 +293,6 @@ extension LiveActivityBridge {
             return
         }
 
-        // backfill latest glucose if contained in this update
         if glucose.count > 1 {
             latestGlucose = glucose.dropFirst().first
         }

+ 8 - 4
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -15,6 +15,10 @@ protocol NightscoutManager: GlucoseSource {
     func deleteManualGlucose(withID id: String) async
     func uploadStatus() async
     func uploadGlucose() async
+    func uploadCarbs() async
+    func uploadPumpHistory() async
+    func uploadOverrides() async
+    func uploadTempTargets() async
     func uploadManualGlucose() async
     func uploadProfiles() async
     func importSettings() async -> ScheduledNightscoutProfile?
@@ -692,24 +696,24 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
     }
 
-    private func uploadPumpHistory() async {
+    func uploadPumpHistory() async {
         await uploadTreatments(
             pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout(),
             fileToSave: OpenAPS.Nightscout.uploadedPumphistory
         )
     }
 
-    private func uploadCarbs() async {
+    func uploadCarbs() async {
         await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
         await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
     }
 
-    private func uploadOverrides() async {
+    func uploadOverrides() async {
         await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
         await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
     }
 
-    private func uploadTempTargets() async {
+    func uploadTempTargets() async {
         await uploadTreatments(
             tempTargetsStorage.nightscoutTreatmentsNotUploaded(),
             fileToSave: OpenAPS.Nightscout.uploadedTempTargets

+ 21 - 17
Gemfile.lock

@@ -10,20 +10,20 @@ GEM
     artifactory (3.0.17)
     atomos (0.1.3)
     aws-eventstream (1.3.0)
-    aws-partitions (1.981.0)
-    aws-sdk-core (3.209.1)
+    aws-partitions (1.1007.0)
+    aws-sdk-core (3.213.0)
       aws-eventstream (~> 1, >= 1.3.0)
-      aws-partitions (~> 1, >= 1.651.0)
+      aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.94.0)
-      aws-sdk-core (~> 3, >= 3.207.0)
+    aws-sdk-kms (1.95.0)
+      aws-sdk-core (~> 3, >= 3.210.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.166.0)
-      aws-sdk-core (~> 3, >= 3.207.0)
+    aws-sdk-s3 (1.171.0)
+      aws-sdk-core (~> 3, >= 3.210.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
-    aws-sigv4 (1.10.0)
+    aws-sigv4 (1.10.1)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
     base64 (0.2.0)
@@ -69,7 +69,7 @@ GEM
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
     fastimage (2.3.1)
-    fastlane (2.223.1)
+    fastlane (2.225.0)
       CFPropertyList (>= 2.3, < 4.0.0)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
@@ -85,6 +85,7 @@ GEM
       faraday-cookie_jar (~> 0.0.6)
       faraday_middleware (~> 1.0)
       fastimage (>= 2.1.0, < 3.0.0)
+      fastlane-sirp (>= 1.0.0)
       gh_inspector (>= 1.1.2, < 2.0.0)
       google-apis-androidpublisher_v3 (~> 0.3)
       google-apis-playcustomapp_v1 (~> 0.1)
@@ -110,6 +111,8 @@ GEM
       xcodeproj (>= 1.13.0, < 2.0.0)
       xcpretty (~> 0.3.0)
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+    fastlane-sirp (1.0.0)
+      sysrandom (~> 1.0)
     gh_inspector (1.1.3)
     google-apis-androidpublisher_v3 (0.54.0)
       google-apis-core (>= 0.11.0, < 2.a)
@@ -152,17 +155,17 @@ GEM
       domain_name (~> 0.5)
     httpclient (2.8.3)
     jmespath (1.6.2)
-    json (2.7.2)
-    jwt (2.9.1)
+    json (2.7.6)
+    jwt (2.9.3)
       base64
     mini_magick (4.13.2)
     mini_mime (1.1.5)
     multi_json (1.15.0)
     multipart-post (2.4.1)
-    nanaimo (0.3.0)
+    nanaimo (0.4.0)
     naturally (2.2.1)
     nkf (0.2.0)
-    optparse (0.5.0)
+    optparse (0.6.0)
     os (1.1.4)
     plist (3.7.1)
     public_suffix (5.1.1)
@@ -172,7 +175,7 @@ GEM
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
     retriable (3.1.2)
-    rexml (3.3.7)
+    rexml (3.3.9)
     rouge (2.0.7)
     ruby2_keywords (0.0.5)
     rubyzip (2.3.2)
@@ -185,6 +188,7 @@ GEM
     simctl (1.6.10)
       CFPropertyList
       naturally
+    sysrandom (1.0.5)
     terminal-notifier (2.0.0)
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
@@ -197,13 +201,13 @@ GEM
     unf (0.2.0)
     unicode-display_width (2.6.0)
     word_wrap (1.0.0)
-    xcodeproj (1.25.0)
+    xcodeproj (1.27.0)
       CFPropertyList (>= 2.3.3, < 4.0)
       atomos (~> 0.1.3)
       claide (>= 1.0.2, < 2.0)
       colored2 (~> 3.1)
-      nanaimo (~> 0.3.0)
-      rexml (>= 3.3.2, < 4.0)
+      nanaimo (~> 0.4.0)
+      rexml (>= 3.3.6, < 4.0)
     xcpretty (0.3.0)
       rouge (~> 2.0.7)
     xcpretty-travis-formatter (1.0.1)

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift

@@ -20,7 +20,7 @@ struct LiveActivityUpdatedLabelView: View {
     }
 
     var body: some View {
-        let dateText = Text("\(dateFormatter.string(from: context.state.date))")
+        let dateText = Text("\((context.state.date != nil) ? dateFormatter.string(from: context.state.date!) : "--")")
 
         if isDetailedLayout {
             VStack {

+ 0 - 1
Model/CoreDataStack.swift

@@ -18,7 +18,6 @@ class CoreDataStack: ObservableObject {
             object: nil,
             queue: nil
         ) { _ in
-            debugPrint("Received a persistent store remote change notification")
             Task {
                 await self.fetchPersistentHistory()
             }