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

Merge branch 'dnzxy:core-data-sync-trio' into core-data-sync-trio

tmhastings 1 год назад
Родитель
Сommit
df350c214a
21 измененных файлов с 334 добавлено и 286 удалено
  1. 8 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 2 23
      FreeAPS/Sources/Helpers/MainChartHelper.swift
  3. 3 3
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  4. 7 0
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  5. 0 12
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  6. 0 2
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  7. 0 10
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  8. 95 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift
  9. 18 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/StartEndMarkerSetup.swift
  10. 5 14
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  11. 6 6
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift
  12. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift
  13. 2 2
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/DummyCharts.swift
  14. 4 109
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseTargetsView.swift
  15. 7 14
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  16. 13 11
      FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift
  17. 40 18
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  18. 107 51
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  19. 5 6
      FreeAPS/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  20. 4 4
      FreeAPS/Sources/Modules/MealSettings/View/MealSettingsRootView.swift
  21. 7 0
      FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -319,6 +319,8 @@
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
+		BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */; };
+		BD4E1A7C2D3686D900D21626 /* StartEndMarkerSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */; };
 		BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */; };
 		BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */; };
 		BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */; };
@@ -1022,6 +1024,8 @@
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
+		BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTargetSetup.swift; sourceTree = "<group>"; };
+		BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartEndMarkerSetup.swift; sourceTree = "<group>"; };
 		BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
 		BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetConfiguration.swift; sourceTree = "<group>"; };
 		BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+helper.swift"; sourceTree = "<group>"; };
@@ -2353,6 +2357,8 @@
 		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */,
+				BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */,
 				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
 				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
@@ -3618,6 +3624,7 @@
 				190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */,
 				DDF847EA2C5DABAC0049BB3B /* WatchConfigGarminView.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
+				BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */,
 				BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
@@ -3803,6 +3810,7 @@
 				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
 				BDC531162D10629000088832 /* ContactPicture.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
+				BD4E1A7C2D3686D900D21626 /* StartEndMarkerSetup.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				BDF34F852C10C62E00D51995 /* GlucoseData.swift in Sources */,
 				19E1F7EC29D082FE005C8D20 /* IconConfigStateModel.swift in Sources */,

+ 2 - 23
FreeAPS/Sources/Helpers/MainChartHelper.swift

@@ -109,7 +109,7 @@ extension MainChartView {
         RuleMark(
             x: .value(
                 "",
-                startMarker,
+                state.startMarker,
                 unit: .second
             )
         ).foregroundStyle(Color.clear)
@@ -119,7 +119,7 @@ extension MainChartView {
         RuleMark(
             x: .value(
                 "",
-                endMarker,
+                state.endMarker,
                 unit: .second
             )
         ).foregroundStyle(Color.clear)
@@ -181,29 +181,8 @@ extension MainChartView {
             }
         }
     }
-}
-
-// MARK: - Calculations and formatting
 
-extension MainChartView {
     func fullWidth(viewWidth: CGFloat) -> CGFloat {
         viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
     }
-
-    // Update start and  end marker to fix scroll update problem with x axis
-    func updateStartEndMarkers() {
-        startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
-
-        let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
-
-        // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
-        let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
-            Int(1.5) * 5 * state
-                .minCount * 60
-        ))
-
-        endMarker = state
-            .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
-            dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
-    }
 }

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

@@ -40,7 +40,7 @@ struct DecimalPickerSettings {
         value: 0.5,
         step: 0.05,
         min: 0.1,
-        max: 2,
+        max: 1.2,
         type: PickerSetting.PickerSettingType.factor
     )
     var high = PickerSetting(value: 180, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
@@ -130,8 +130,8 @@ struct DecimalPickerSettings {
     )
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var delay = PickerSetting(value: 60, step: 15, min: 30, max: 120, type: PickerSetting.PickerSettingType.minute)
-    var minuteInterval = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
+    var delay = PickerSetting(value: 60, step: 10, min: 60, max: 120, type: PickerSetting.PickerSettingType.minute)
+    var minuteInterval = PickerSetting(value: 30, step: 5, min: 10, max: 60, type: PickerSetting.PickerSettingType.minute)
     var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, type: PickerSetting.PickerSettingType.hour)

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

@@ -4,6 +4,7 @@ import SwiftUI
 extension BasalProfileEditor {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
+        @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
 
         var syncInProgress: Bool = false
         var initialItems: [Item] = []
@@ -105,6 +106,12 @@ extension BasalProfileEditor {
                     print("We were successful")
                 }
                 .store(in: &lifetime)
+
+            DispatchQueue.main.async {
+                self.broadcaster.notify(BasalProfileObserver.self, on: .main) {
+                    $0.basalProfileDidChange(profile)
+                }
+            }
         }
 
         @MainActor func validate() {

+ 0 - 12
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -110,18 +110,6 @@ extension DataTable {
                 .navigationTitle("History")
                 .navigationBarTitleDisplayMode(.large)
                 .toolbar {
-                    ToolbarItem(placement: .topBarLeading, content: {
-                        Button(
-                            action: { state.showModal(for: .statistics) },
-                            label: {
-                                HStack {
-                                    Text("Statistics")
-                                }
-                            }
-                        )
-                    })
-                }
-                .toolbar {
                     ToolbarItem(placement: .topBarTrailing, content: {
                         addButton({
                             showManualGlucose = true

+ 0 - 2
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -9,8 +9,6 @@ protocol HomeProvider: Provider {
     func heartbeatNow()
     func pumpSettings() async -> PumpSettings
     func getBasalProfile() async -> [BasalProfileEntry]
-    func tempTargets(hours: Int) -> [TempTarget]
     func pumpReservoir() async -> Decimal?
-    func tempTarget() -> TempTarget?
     func getBGTargets() async -> BGTargets
 }

+ 0 - 10
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -16,16 +16,6 @@ extension Home {
             apsManager.heartbeat(date: Date())
         }
 
-        func tempTargets(hours: Int) -> [TempTarget] {
-            tempTargetsStorage.recent().filter {
-                $0.createdAt.addingTimeInterval(hours.hours.timeInterval) > Date()
-            }
-        }
-
-        func tempTarget() -> TempTarget? {
-            tempTargetsStorage.current()
-        }
-
         func pumpSettings() async -> PumpSettings {
             await storage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))

+ 95 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift

@@ -0,0 +1,95 @@
+import Foundation
+
+extension Home.StateModel {
+    /**
+     Processes raw glucose target data into a list of target profiles for visualization.
+
+     - Parameters:
+        - rawTargets: The raw glucose target data containing offset and glucose values.
+        - startMarker: The reference date to start the target profiles from.
+     - Returns: An array of `TargetProfile` objects, each representing a glucose target range starting from the day of the startMarker and ending two days later.
+
+     The function:
+     - Converts glucose targets into profiles covering three consecutive days (day of startMarker, day after startMarker and day after that).
+     - Calculates start and end times for each target based on the offsets provided.
+     - Handles conversions between mg/dL and mmol/L as per user settings.
+     - Ensures targets span across midnight to avoid data cutoff.
+     */
+    func processFetchedTargets(_ rawTargets: BGTargets, startMarker: Date) -> [TargetProfile] {
+        var targetProfiles: [TargetProfile] = []
+
+        // Ensure there are targets to process
+        guard !rawTargets.targets.isEmpty else {
+            print("Warning: No targets to process in rawTargets.")
+            return []
+        }
+
+        let targets = rawTargets.targets
+
+        // Base date is the start of the day for the startMarker
+        let baseDate = Calendar.current.startOfDay(for: startMarker)
+
+        // Process each target three times
+        for index in 0 ..< (targets.count * 3) {
+            // Calculate the day offset (0 for today, 1 for tomorrow, 2 for day after)
+            let dayOffset = index / targets.count
+            let targetIndex = index % targets.count
+
+            // Validate target index to ensure safety
+            guard targetIndex < targets.count else {
+                print("Error: Invalid target index \(targetIndex).")
+                continue
+            }
+
+            // Fetch the target for the current iteration
+            let target = targets[targetIndex]
+
+            // Calculate the time offset for the current day
+            let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60)
+
+            // Calculate the start time for the current target
+            let startTime = baseDate
+                .addingTimeInterval(dayTimeOffset)
+                .addingTimeInterval(TimeInterval(target.offset * 60))
+
+            // Calculate the end time for the current target
+            let endTime: Date = {
+                if targetIndex + 1 < targets.count {
+                    // End time is the start time of the next target within the same day
+                    return baseDate
+                        .addingTimeInterval(dayTimeOffset)
+                        .addingTimeInterval(TimeInterval(targets[targetIndex + 1].offset * 60))
+                } else {
+                    // End time is the end of the day (midnight of the next day)
+                    return baseDate.addingTimeInterval(dayTimeOffset + 24 * 60 * 60)
+                }
+            }()
+
+            // Convert glucose value based on user unit preference (mg/dL or mmol/L)
+            let targetValue = units == .mgdL ? target.low : target.low.asMmolL
+
+            // Append the processed target profile to the list
+            targetProfiles.append(
+                TargetProfile(
+                    value: targetValue,
+                    startTime: startTime.timeIntervalSinceReferenceDate,
+                    endTime: endTime.timeIntervalSinceReferenceDate
+                )
+            )
+        }
+
+        return targetProfiles
+    }
+}
+
+struct TargetProfile: Hashable {
+    let value: Decimal
+    let startTime: TimeInterval
+    let endTime: TimeInterval
+}
+
+private extension Date {
+    var startOfDay: Date {
+        Calendar.current.startOfDay(for: self)
+    }
+}

+ 18 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/StartEndMarkerSetup.swift

@@ -0,0 +1,18 @@
+import Foundation
+
+extension Home.StateModel {
+    // Update start and  end marker to fix scroll update problem with x axis
+    func updateStartEndMarkers() {
+        startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
+
+        let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
+
+        // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
+        let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
+            Int(1.5) * 5 * minCount * 60
+        ))
+
+        endMarker = forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
+            dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
+    }
+}

+ 5 - 14
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -19,6 +19,8 @@ extension Home {
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
+        var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
+        var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
         var manualGlucose: [BloodGlucose] = []
         var uploadStats = false
         var recentGlucose: BloodGlucose?
@@ -26,7 +28,7 @@ extension Home {
         var basalProfile: [BasalProfileEntry] = []
         var bgTargets = BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
             ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
-        var tempTargets: [TempTarget] = []
+        var targetProfiles: [TargetProfile] = []
         var timerDate = Date()
         var closedLoop = false
         var pumpSuspended = false
@@ -37,7 +39,6 @@ extension Home {
         var reservoir: Decimal?
         var pumpName = ""
         var pumpExpiresAtDate: Date?
-        var tempTarget: TempTarget?
         var highTTraisesSens: Bool = false
         var lowTTlowersSens: Bool = false
         var isExerciseModeActive: Bool = false
@@ -267,7 +268,6 @@ extension Home {
         }
 
         private func registerObservers() {
-            broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(PreferencesObserver.self, observer: self)
@@ -479,8 +479,10 @@ extension Home {
 
         private func setupGlucoseTargets() async {
             let bgTargets = await provider.getBGTargets()
+            let targetProfiles = processFetchedTargets(bgTargets, startMarker: startMarker)
             await MainActor.run {
                 self.bgTargets = bgTargets
+                self.targetProfiles = targetProfiles
             }
         }
 
@@ -554,7 +556,6 @@ extension Home {
 }
 
 extension Home.StateModel:
-    GlucoseObserver,
     DeterminationObserver,
     SettingsObserver,
     PreferencesObserver,
@@ -565,11 +566,6 @@ extension Home.StateModel:
     PumpTimeZoneObserver,
     PumpDeactivatedObserver
 {
-    // TODO: still needed?
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-//        setupGlucose()
-    }
-
     func determinationDidUpdate(_: Determination) {
         waitForSuggestion = false
     }
@@ -606,11 +602,6 @@ extension Home.StateModel:
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
     }
 
-    // TODO: is this ever really triggered? react to MOC changes?
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        displayPumpStatusHighlightMessage()
-    }
-
     func pumpSettingsDidChange(_: PumpSettings) {
         Task {
             await setupPumpSettings()

+ 6 - 6
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift

@@ -34,7 +34,7 @@ extension MainChartView {
             }
             .frame(minHeight: geo.size.height * 0.05)
             .frame(width: fullWidth(viewWidth: screenSize.width))
-            .chartXScale(domain: startMarker ... endMarker)
+            .chartXScale(domain: state.startMarker ... state.endMarker)
             .chartXAxis { basalChartXAxis }
             .chartXAxis(.hidden)
             .chartYAxis(.hidden)
@@ -96,7 +96,7 @@ extension MainChartView {
                 series: .value("profile", "profile")
             ).lineStyle(.init(lineWidth: 2, dash: [2, 4])).foregroundStyle(Color.insulin)
             LineMark(
-                x: .value("End Date", profile.endDate ?? endMarker),
+                x: .value("End Date", profile.endDate ?? state.endMarker),
                 y: .value("Amount", profile.amount),
                 series: .value("profile", "profile")
             ).lineStyle(.init(lineWidth: 2.5, dash: [2, 4])).foregroundStyle(Color.insulin)
@@ -204,7 +204,7 @@ extension MainChartView {
 
             async let getRegularBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
-                timeEnd: endMarker.timeIntervalSince1970
+                timeEnd: state.endMarker.timeIntervalSince1970
             )
 
             var regularPoints = await getRegularBasalPoints
@@ -224,8 +224,8 @@ extension MainChartView {
                     BasalProfile(
                         amount: single.amount,
                         isOverwritten: single.isOverwritten,
-                        startDate: startMarker,
-                        endDate: endMarker
+                        startDate: state.startMarker,
+                        endDate: state.endMarker
                     )
                 )
             }
@@ -248,7 +248,7 @@ extension MainChartView {
                             amount: lastItem.amount,
                             isOverwritten: lastItem.isOverwritten,
                             startDate: lastItem.startDate,
-                            endDate: endMarker
+                            endDate: state.endMarker
                         )
                     )
                 }

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

@@ -46,7 +46,7 @@ extension MainChartView {
         .chartLegend(.hidden)
         .frame(minHeight: geo.size.height * 0.12)
         .frame(width: fullWidth(viewWidth: screenSize.width))
-        .chartXScale(domain: startMarker ... endMarker)
+        .chartXScale(domain: state.startMarker ... state.endMarker)
         .chartXSelection(value: $selection)
         .chartXAxis { basalChartXAxis }
         .chartYAxis { cobIobChartYAxis }

+ 2 - 2
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/DummyCharts.swift

@@ -43,7 +43,7 @@ extension MainChartView {
         )
         .frame(width: screenSize.width - 10)
         .chartXAxis { mainChartXAxis }
-        .chartXScale(domain: startMarker ... endMarker)
+        .chartXScale(domain: state.startMarker ... state.endMarker)
         .chartXAxis(.hidden)
         .chartYAxis { mainChartYAxis }
         .chartYScale(
@@ -69,7 +69,7 @@ extension MainChartView {
             .id("DummyCobChart")
             .frame(minHeight: geo.size.height * 0.12)
             .frame(width: screenSize.width - 10)
-            .chartXScale(domain: startMarker ... endMarker)
+            .chartXScale(domain: state.startMarker ... state.endMarker)
             .chartXAxis { basalChartXAxis }
             .chartXAxis(.hidden)
             .chartYAxis { cobIobChartYAxis }

+ 4 - 109
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseTargetsView.swift

@@ -3,30 +3,20 @@ import Foundation
 import SwiftUI
 
 struct GlucoseTargetsView: ChartContent {
-    let startMarker: Date
-    let units: GlucoseUnits
-    let bgTargets: BGTargets
+    let targetProfiles: [TargetProfile]
 
     var body: some ChartContent {
-        drawGlucoseTargets()
+        drawGlucoseTargets(for: targetProfiles)
     }
 
     /**
      Draws glucose target ranges on the chart
 
      - Returns: A ChartContent containing line marks representing target glucose ranges
-
-     The function:
-     - Creates target profiles for two consecutive days
-     - Converts values between mg/dL and mmol/L based on user settings
-     - Draws green lines to visualize the target ranges
      */
-    private func drawGlucoseTargets() -> some ChartContent {
-        // Array to store target profiles for visualization
-        let targetProfiles: [TargetProfile] = processFetchedTargets(bgTargets)
-
+    private func drawGlucoseTargets(for targetProfiles: [TargetProfile]) -> some ChartContent {
         // Draw target lines for each profile
-        return ForEach(targetProfiles, id: \.self) { profile in
+        ForEach(targetProfiles, id: \.self) { profile in
             LineMark(
                 x: .value("Time", Date(timeIntervalSinceReferenceDate: profile.startTime)),
                 y: .value("Target", profile.value)
@@ -42,99 +32,4 @@ struct GlucoseTargetsView: ChartContent {
             .foregroundStyle(Color.green.gradient)
         }
     }
-
-    /**
-     Processes raw glucose target data into a list of target profiles for visualization.
-
-     - Parameter rawTargets: The raw glucose target data containing offset and glucose values.
-     - Returns: An array of `TargetProfile` objects, each representing a glucose target range for today and tomorrow.
-
-     The function:
-     - Converts glucose targets into profiles covering two consecutive days (today and tomorrow).
-     - Calculates start and end times for each target based on the offsets provided.
-     - Handles conversions between mg/dL and mmol/L as per user settings.
-     - Ensures targets span across midnight to avoid data cutoff.
-
-     Example:
-     For a target at offset 0 (midnight) with low glucose value 70 mg/dL, the function generates two profiles:
-     - One for today from midnight to the next target offset or end of the day.
-     - Another for tomorrow covering the same time range.
-     */
-    private func processFetchedTargets(_ rawTargets: BGTargets) -> [TargetProfile] {
-        var targetProfiles: [TargetProfile] = []
-
-        // Ensure there are targets to process
-        guard !rawTargets.targets.isEmpty else {
-            print("Warning: No targets to process in rawTargets.")
-            return []
-        }
-
-        let targets = rawTargets.targets
-
-        // Base date is the start of the day for the startMarker
-        let baseDate = Calendar.current.startOfDay(for: startMarker)
-
-        // Process each target twice: once for today and once for tomorrow
-        for index in 0 ..< (targets.count * 2) {
-            // Calculate the day offset (0 for today, 1 for tomorrow)
-            let dayOffset = index / targets.count
-            let targetIndex = index % targets.count
-
-            // Validate target index to ensure safety
-            guard targetIndex < targets.count else {
-                print("Error: Invalid target index \(targetIndex).")
-                continue
-            }
-
-            // Fetch the target for the current iteration
-            let target = targets[targetIndex]
-
-            // Calculate the time offset for the current day
-            let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60)
-
-            // Calculate the start time for the current target
-            let startTime = baseDate
-                .addingTimeInterval(dayTimeOffset)
-                .addingTimeInterval(TimeInterval(target.offset * 60))
-
-            // Calculate the end time for the current target
-            let endTime: Date = {
-                if targetIndex + 1 < targets.count {
-                    // End time is the start time of the next target within the same day
-                    return baseDate
-                        .addingTimeInterval(dayTimeOffset)
-                        .addingTimeInterval(TimeInterval(targets[targetIndex + 1].offset * 60))
-                } else {
-                    // End time is the end of the day (midnight of the next day)
-                    return baseDate.addingTimeInterval(dayTimeOffset + 24 * 60 * 60)
-                }
-            }()
-
-            // Convert glucose value based on user unit preference (mg/dL or mmol/L)
-            let targetValue = units == .mgdL ? target.low : target.low.asMmolL
-
-            // Append the processed target profile to the list
-            targetProfiles.append(
-                TargetProfile(
-                    value: targetValue,
-                    startTime: startTime.timeIntervalSinceReferenceDate,
-                    endTime: endTime.timeIntervalSinceReferenceDate
-                )
-            )
-        }
-
-        return targetProfiles
-    }
-}
-
-struct TargetProfile: Hashable {
-    let value: Decimal
-    let startTime: TimeInterval
-    let endTime: TimeInterval
-}
-
-private extension Date {
-    var startOfDay: Date {
-        Calendar.current.startOfDay(for: self)
-    }
 }

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

@@ -10,7 +10,6 @@ struct MainChartView: View {
     var safeAreaSize: CGFloat
     var units: GlucoseUnits
     var hours: Int
-    var tempTargets: [TempTarget]
     var highGlucose: Decimal
     var lowGlucose: Decimal
     var currentGlucoseTarget: Decimal
@@ -23,10 +22,6 @@ struct MainChartView: View {
 
     @State var basalProfiles: [BasalProfile] = []
     @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
-    @State var startMarker =
-        Date(timeIntervalSinceNow: TimeInterval(hours: -24))
-    @State var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
-
     @State var selection: Date? = nil
 
     @State var mainChartHasInitialized = false
@@ -88,7 +83,7 @@ struct MainChartView: View {
                         }
                         .onChange(of: state.glucoseFromPersistence.last?.glucose) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
-                            updateStartEndMarkers()
+                            state.updateStartEndMarkers()
                         }
                         .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
@@ -100,7 +95,7 @@ struct MainChartView: View {
                         .onAppear {
                             if !mainChartHasInitialized {
                                 scroller.scrollTo("MainChart", anchor: .trailing)
-                                updateStartEndMarkers()
+                                state.updateStartEndMarkers()
                                 calculateTempBasalsInBackground()
                                 mainChartHasInitialized = true
                             }
@@ -122,6 +117,10 @@ extension MainChartView {
                 drawEndRuleMark()
                 drawCurrentTimeMarker()
 
+                GlucoseTargetsView(
+                    targetProfiles: state.targetProfiles
+                )
+
                 OverrideView(
                     state: state,
                     overrides: state.overrides,
@@ -137,12 +136,6 @@ extension MainChartView {
                     viewContext: context
                 )
 
-                GlucoseTargetsView(
-                    startMarker: startMarker,
-                    units: state.units,
-                    bgTargets: state.bgTargets
-                )
-
                 GlucoseChartView(
                     glucoseData: state.glucoseFromPersistence,
                     units: state.units,
@@ -198,7 +191,7 @@ extension MainChartView {
                 minHeight: geo.size.height * (0.28 - safeAreaSize)
             )
             .frame(width: fullWidth(viewWidth: screenSize.width))
-            .chartXScale(domain: startMarker ... endMarker)
+            .chartXScale(domain: state.startMarker ... state.endMarker)
             .chartXAxis { mainChartXAxis }
             .chartYAxis { mainChartYAxis }
             .chartYAxis(.hidden)

+ 13 - 11
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -4,6 +4,8 @@ import SwiftUI
 import UIKit
 
 struct LoopView: View {
+    @Environment(\.colorScheme) var colorScheme
+
     private enum Config {
         static let lag: TimeInterval = 30
     }
@@ -19,10 +21,19 @@ struct LoopView: View {
     private let rect = CGRect(x: 0, y: 0, width: 18, height: 18)
 
     var body: some View {
+        loopStatusWithMinutes
+            .padding(.vertical, 5)
+            .padding(.horizontal, 10)
+            .overlay(
+                Capsule()
+                    .stroke(color.opacity(0.4), lineWidth: 2)
+            )
+    }
+
+    private var loopStatusWithMinutes: some View {
         HStack(alignment: .center) {
             ZStack {
-                Image(systemName: "circle")
-                    .mask(mask(in: rect).fill(style: FillStyle(eoFill: true)))
+                Image(systemName: (!closedLoop || manualTempBasal) ? "circle.and.line.horizontal" : "circle")
                 if isLooping {
                     ProgressView()
                 }
@@ -41,7 +52,6 @@ struct LoopView: View {
                 Text("--")
             }
         }
-        .strikethrough(!closedLoop || manualTempBasal, pattern: .solid, color: color)
         .font(.callout).fontWeight(.bold).fontDesign(.rounded)
         .foregroundColor(color)
     }
@@ -80,14 +90,6 @@ struct LoopView: View {
             return .loopRed
         }
     }
-
-    func mask(in rect: CGRect) -> Path {
-        var path = Rectangle().path(in: rect)
-        if !closedLoop || manualTempBasal {
-            path.addPath(Rectangle().path(in: CGRect(x: rect.minX, y: rect.midY - 4, width: rect.width, height: 8)))
-        }
-        return path
-    }
 }
 
 extension View {

+ 40 - 18
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -45,24 +45,44 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "cross.vial.fill")
                             .font(.callout)
-                            .foregroundColor(reservoirColor)
+
                         if reservoir == 0xDEAD_BEEF {
                             Text("50+ " + NSLocalizedString("U", comment: "Insulin unit"))
-                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                .font(.callout)
+                                .fontWeight(.bold)
+                                .fontDesign(.rounded)
                         } else {
                             Text(
                                 Formatter.integerFormatter
                                     .string(from: reservoir as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
                             )
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            .font(.callout)
+                            .fontWeight(.bold)
+                            .fontDesign(.rounded)
                         }
                     }
+                    .padding(.vertical, 5)
+                    .padding(.horizontal, 10)
+                    .foregroundStyle(reservoirColor)
+                    .overlay(
+                        Capsule()
+                            .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
+                    )
 
                     if let timeZone = timeZone, timeZone.secondsFromGMT() != TimeZone.current.secondsFromGMT() {
-                        Image(systemName: "clock.badge.exclamationmark.fill")
-                            .font(.callout)
-                            .symbolRenderingMode(.palette)
-                            .foregroundStyle(.red, Color(.warning))
+                        HStack {
+                            Image(systemName: "clock.badge.exclamationmark.fill")
+                                .font(.callout)
+                                .symbolRenderingMode(.palette)
+                                .foregroundStyle(.red, Color(.warning))
+
+                            Text("Timezone")
+                                .font(.callout)
+                                .fontWeight(.bold)
+                                .fontDesign(.rounded)
+                                .foregroundStyle(.red)
+                        }
+                        .padding(.leading, 12)
                     }
                 }
 
@@ -70,7 +90,7 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "battery.100")
                             .font(.callout)
-                            .foregroundColor(batteryColor)
+                            .foregroundStyle(batteryColor)
                         Text("\(Int(battery.first?.percent ?? 100)) %")
                             .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
@@ -80,13 +100,15 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "stopwatch.fill")
                             .font(.callout)
-                            .foregroundColor(timerColor)
+                            .foregroundStyle(timerColor)
 
                         Text(remainingTimeString(time: date.timeIntervalSince(timerDate)))
                             .font(!(date.timeIntervalSince(timerDate) > 0) ? .subheadline : .callout)
                             .fontWeight(.bold)
                             .fontDesign(.rounded)
                     }
+                    // aligns the stopwatch icon exactly with the first pixel of the reservoir icon
+                    .padding(.leading, 12)
                 }
             }
         }
@@ -123,11 +145,11 @@ struct PumpView: View {
 
         switch battery.percent {
         case ...10:
-            return .red
+            return Color.loopRed
         case ...20:
-            return .yellow
+            return Color.orange
         default:
-            return .green
+            return Color.loopGreen
         }
     }
 
@@ -138,11 +160,11 @@ struct PumpView: View {
 
         switch reservoir {
         case ...10:
-            return .red
+            return Color.loopRed
         case ...30:
-            return .yellow
+            return Color.orange
         default:
-            return .blue
+            return Color.insulin
         }
     }
 
@@ -155,11 +177,11 @@ struct PumpView: View {
 
         switch time {
         case ...8.hours.timeInterval:
-            return .red
+            return Color.loopRed
         case ...1.days.timeInterval:
-            return .yellow
+            return Color.orange
         default:
-            return .green
+            return Color.loopGreen
         }
     }
 }

+ 107 - 51
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -5,11 +5,9 @@ import SwiftUI
 import Swinject
 
 struct TimePicker: Identifiable {
-    let label: String
-    let number: String
     var active: Bool
     let hours: Int16
-    var id: String { label }
+    var id: String { hours.description }
 }
 
 extension Home {
@@ -36,15 +34,12 @@ extension Home {
         @State var showPumpSelection: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
-            TimePicker(label: "2 hours", number: "2", active: false, hours: 2),
-            TimePicker(label: "4 hours", number: "4", active: false, hours: 4),
-            TimePicker(label: "6 hours", number: "6", active: false, hours: 6),
-            TimePicker(label: "12 hours", number: "12", active: false, hours: 12),
-            TimePicker(label: "24 hours", number: "24", active: false, hours: 24)
+            TimePicker(active: false, hours: 4),
+            TimePicker(active: false, hours: 6),
+            TimePicker(active: false, hours: 12),
+            TimePicker(active: false, hours: 24)
         ]
 
-        let buttonFont = Font.custom("TimeButtonFont", size: 14)
-
         @FetchRequest(fetchRequest: OverrideStored.fetch(
             NSPredicate.lastActiveOverride,
             ascending: false,
@@ -116,7 +111,8 @@ extension Home {
                 timeZone: state.timeZone,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 battery: state.batteryFromPersistence
-            ).onTapGesture {
+            )
+            .onTapGesture {
                 if state.pumpDisplayState == nil {
                     // shows user confirmation dialog with pump model choices, then proceeds to setup
                     showPumpSelection.toggle()
@@ -245,45 +241,73 @@ extension Home {
             return components.isEmpty ? nil : components.joined(separator: ", ")
         }
 
-        var timeInterval: some View {
-            HStack(alignment: .center) {
+        var timeIntervalButtons: some View {
+            let buttonColor = (colorScheme == .dark ? Color.white : Color.black).opacity(0.8)
+
+            return HStack(alignment: .center) {
                 ForEach(timeButtons) { button in
-                    Text(button.active ? NSLocalizedString(button.label, comment: "") : button.number).onTapGesture {
+                    Button(action: {
                         state.hours = button.hours
+                    }) {
+                        Group {
+                            if button.active {
+                                Text(
+                                    NSLocalizedString(button.hours.description, comment: "") + " " +
+                                        NSLocalizedString("h", comment: "h")
+                                )
+                            } else {
+                                Text(NSLocalizedString(button.hours.description, comment: ""))
+                            }
+                        }
+                        .font(.footnote)
+                        .fontWeight(button.active ? .semibold : .regular)
+                        .padding(.vertical, 5)
+                        .padding(.horizontal, 10)
+                        .foregroundColor(
+                            button
+                                .active ? (colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white) : buttonColor
+                        )
+                        .background(button.active ? buttonColor.opacity(colorScheme == .dark ? 1 : 0.8) : Color.clear)
+                        .clipShape(Capsule())
+                        .overlay(
+                            Capsule()
+                                .stroke(button.active ? buttonColor.opacity(0.4) : Color.clear, lineWidth: 2)
+                        )
                     }
-                    .foregroundStyle(button.active ? (colorScheme == .dark ? Color.white : Color.black).opacity(0.9) : .secondary)
-                    .frame(maxHeight: 30).padding(.horizontal, 8)
-                    .background(
-                        button.active ?
-                            // RGB(30, 60, 95)
-                            (
-                                colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
-                                    Color.white
-                            ) :
-                            Color
-                            .clear
-                    )
-                    .cornerRadius(20)
                 }
-                Button(action: {
-                    state.isLegendPresented.toggle()
-                }) {
-                    Image(systemName: "info")
-                        .foregroundColor(colorScheme == .dark ? Color.white : Color.black).opacity(0.9)
-                        .frame(width: 20, height: 20)
-                        .background(
-                            colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
-                                Color.white
-                        )
-                        .clipShape(Circle())
+            }
+        }
+
+        var statsIconString: String {
+            if #available(iOS 18, *) {
+                return "chart.line.text.clipboard"
+            } else {
+                return "list.clipboard"
+            }
+        }
+
+        @ViewBuilder private func tappableButton(
+            buttonColor: Color,
+            label: String,
+            iconString: String,
+            action: @escaping () -> Void
+        ) -> some View {
+            Button(action: {
+                action()
+            }) {
+                HStack {
+                    Image(systemName: iconString)
+                    Text(label)
                 }
-                .padding([.top, .bottom])
+                .font(.footnote)
+                .padding(.vertical, 5)
+                .padding(.horizontal, 10)
+                .foregroundStyle(buttonColor)
+                .overlay(
+                    Capsule()
+                        .stroke(buttonColor.opacity(0.4), lineWidth: 2)
+                )
             }
-            .shadow(
-                color: Color.black.opacity(colorScheme == .dark ? 0.75 : 0.33),
-                radius: colorScheme == .dark ? 5 : 3
-            )
-            .font(buttonFont)
         }
 
         @ViewBuilder func mainChart(geo: GeometryProxy) -> some View {
@@ -293,7 +317,6 @@ extension Home {
                     safeAreaSize: notificationsDisabled == true ? safeAreaSize : 0,
                     units: state.units,
                     hours: state.filteredHours,
-                    tempTargets: state.tempTargets,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
                     currentGlucoseTarget: state.currentGlucoseTarget,
@@ -324,9 +347,11 @@ extension Home {
                     lastLoopDate: state.lastLoopDate,
                     manualTempBasal: state.manualTempBasal,
                     determination: state.determinationsFromPersistence
-                ).onTapGesture {
+                )
+                .onTapGesture {
                     state.isLoopStatusPresented = true
-                }.onLongPressGesture {
+                }
+                .onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                     impactHeavy.impactOccurred()
                     state.runLoop()
@@ -347,6 +372,8 @@ extension Home {
                             )!
                         ).font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
+                    // aligns the evBG icon exactly with the first pixel of loop status icon
+                    .padding(.leading, 12)
                 } else {
                     HStack {
                         Image(systemName: "arrow.right.circle")
@@ -402,8 +429,17 @@ extension Home {
                         Image(systemName: "drop.circle")
                             .font(.callout)
                             .foregroundColor(.insulinTintColor)
-                        Text(tempBasalString)
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                        if tempBasalString.count > 5 {
+                            Text(tempBasalString)
+                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                .lineLimit(1)
+                                .minimumScaleFactor(0.85)
+                                .truncationMode(.tail)
+                                .allowsTightening(true)
+                        } else {
+                            // Short strings can just display normally
+                            Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                        }
                     } else {
                         Image(systemName: "drop.circle")
                             .font(.callout)
@@ -815,8 +851,28 @@ extension Home {
 
                 mainChart(geo: geo)
 
-                timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
-                    .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
+                HStack {
+                    tappableButton(
+                        buttonColor: (colorScheme == .dark ? Color.white : Color.black).opacity(0.8),
+                        label: "Stats",
+                        iconString: statsIconString,
+                        action: { state.showModal(for: .statistics) }
+                    )
+
+                    Spacer()
+
+                    timeIntervalButtons.padding(.top, UIDevice.adjustPadding(min: 0, max: 10))
+                        .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 10))
+
+                    Spacer()
+
+                    tappableButton(
+                        buttonColor: (colorScheme == .dark ? Color.white : Color.black).opacity(0.8),
+                        label: "Info",
+                        iconString: "info",
+                        action: { state.isLegendPresented.toggle() }
+                    )
+                }.padding([.horizontal, .top, .bottom])
 
                 if let progress = state.bolusProgress {
                     bolusView(geo: geo, progress)

+ 5 - 6
FreeAPS/Sources/Modules/MealSettings/MealSettingsStateModel.swift

@@ -7,10 +7,10 @@ extension MealSettings {
         @Published var maxCarbs: Decimal = 250
         @Published var maxFat: Decimal = 250
         @Published var maxProtein: Decimal = 250
-        @Published var individualAdjustmentFactor: Decimal = 0
-        @Published var timeCap: Decimal = 0
-        @Published var minuteInterval: Decimal = 0
-        @Published var delay: Decimal = 0
+        @Published var individualAdjustmentFactor: Decimal = 0.5
+        @Published var timeCap: Decimal = 8
+        @Published var minuteInterval: Decimal = 30
+        @Published var delay: Decimal = 60
 
         override func subscribe() {
             units = settingsManager.settings.units
@@ -38,8 +38,7 @@ extension MealSettings {
             })
 
             subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor, initial: {
-                let value = max(min($0, 1.2), 0.1)
-                individualAdjustmentFactor = value
+                individualAdjustmentFactor = $0
             }, map: {
                 $0
             })

+ 4 - 4
FreeAPS/Sources/Modules/MealSettings/View/MealSettingsRootView.swift

@@ -288,7 +288,6 @@ extension MealSettings {
                             )
                             Text("Increasing this setting may result in more FPU entries with smaller carb values.")
                             Text("Decreasing this setting may result in fewer FPU entries with larger carb values.")
-                            Text("Note: Accepted range for this setting is 5 - 12 hours.")
                         }
                     )
 
@@ -316,7 +315,6 @@ extension MealSettings {
                             Text("The shorter the interval, the smoother the correlating dosing result.")
                             Text("Increasing this setting may result in fewer FPU entries with larger carb values.")
                             Text("Decreasing this setting may result in more FPU entries with smaller carb values.")
-                            Text("Accepted range for this setting is 5 - 60 minutes.")
                         }
                     )
 
@@ -344,9 +342,11 @@ extension MealSettings {
                                     Text("(Fat × 45%) + (Protein × 20%)")
                                     Text("100% is full effect:").bold()
                                     Text("(Fat × 90%) + (Protein × 40%)")
-                                    Text("200% is double effect:").bold()
-                                    Text("(Fat × 180%) + (Protein x 80%)")
+                                    Text("110% makes fat-to-carbs ratio essentially equal:").bold()
+                                    Text("(Fat × 99%) + (Protein x 44%)")
                                 }
+                                .multilineTextAlignment(.center)
+                                .fixedSize(horizontal: false, vertical: true)
                                 Text(
                                     "Tip: You may find that your normal carb ratio needs to increase to a larger number when you begin adding fat and protein entries. For this reason, it is best to start with a factor of about 50%."
                                 )

+ 7 - 0
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -3,6 +3,7 @@ import SwiftUI
 extension TargetsEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() private var nightscout: NightscoutManager!
+        @Injected() private var broadcaster: Broadcaster!
 
         @Published var items: [Item] = []
         @Published var initialItems: [Item] = []
@@ -75,6 +76,12 @@ extension TargetsEditor {
             provider.saveProfile(profile)
             initialItems = items.map { Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
 
+            DispatchQueue.main.async {
+                self.broadcaster.notify(BGTargetsObserver.self, on: .main) {
+                    $0.bgTargetsDidChange(profile)
+                }
+            }
+
             Task.detached(priority: .low) {
                 debug(.nightscout, "Attempting to upload targets to Nightscout")
                 await self.nightscout.uploadProfiles()