From c209fbd33c444586a634fb3a3cb6d01b9391f52a Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:52:16 +0200 Subject: [PATCH 01/11] Implement target schedule data update functionality --- .../Nightscout/TargetSchedule.swift | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 LoopFollow/Controllers/Nightscout/TargetSchedule.swift diff --git a/LoopFollow/Controllers/Nightscout/TargetSchedule.swift b/LoopFollow/Controllers/Nightscout/TargetSchedule.swift new file mode 100644 index 000000000..a2ae84660 --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/TargetSchedule.swift @@ -0,0 +1,95 @@ +// LoopFollow +// TargetSchedule.swift + +import Foundation + +extension MainViewController { + /// Builds `targetScheduleData`, the routine (non-Override) target-of-the-day line shown on + /// the graph -- mirrors `basalScheduleData` in Profile.swift exactly, substituting the + /// profile's target-high schedule (via `profileManager.targetHighSchedule`, already + /// unit-converted) for the basal schedule. Uses target-high specifically to match + /// `currentTargetHigh()`, the same value already shown in the "Target" info row, so the line + /// never disagrees with the number beside it. + /// + /// Like the basal line, this only ever reflects the day's *routine* schedule -- an active + /// Override, Temp Target, or Weekend Profile is shown separately as a colored band + /// (`overrides`/`tempTargets` in BGChartModel), the same relationship Trio's own target line + /// has with its own Override bands. + func updateTargetScheduleData() { + let targetSchedule = profileManager.targetHighSchedule + guard !targetSchedule.isEmpty else { + targetScheduleData.removeAll() + return + } + + var targetSegments: [DataStructs.targetProfileSegment] = [] + + let graphHours = 24 * Storage.shared.downloadDays.value + // Build scheduled target segments from right to left by moving pointers to the current + // midnight and current target -- same walk as the basal schedule builder. + var midnight = dateTimeUtils.getTimeIntervalMidnightToday() + var targetIndex = targetSchedule.count - 1 + var start = midnight + Double(targetSchedule[targetIndex].timeAsSeconds) + var end = dateTimeUtils.getNowTimeIntervalUTC() + while start > end { + targetIndex -= 1 + start = midnight + Double(targetSchedule[targetIndex].timeAsSeconds) + } + let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) + while end >= graphStart { + let entry = DataStructs.targetProfileSegment( + targetHigh: targetSchedule[targetIndex].value.doubleValue(for: .milligramsPerDeciliter), + startDate: start, endDate: end + ) + targetSegments.append(entry) + + targetIndex -= 1 + if targetIndex < 0 { + targetIndex = targetSchedule.count - 1 + midnight = midnight.advanced(by: -24 * 60 * 60) + } + end = start - 1 + start = midnight + Double(targetSchedule[targetIndex].timeAsSeconds) + } + targetSegments.reverse() + + var firstPass = true + let predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * Storage.shared.predictionToLoad.value) + targetScheduleData.removeAll() + + for i in 0 ..< targetSegments.count { + let timeStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) + + if firstPass == false, + targetSegments[i].startDate <= predictionEndTime + { + let startDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: targetSegments[i].startDate) + targetScheduleData.append(startDot) + var endDate = targetSegments[i].endDate + + if endDate > predictionEndTime || i == targetSegments.count - 1 { + endDate = Double(predictionEndTime) + } + + let endDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: endDate) + targetScheduleData.append(endDot) + } + + if firstPass == true { + if timeStart >= targetSegments[i].startDate, timeStart < targetSegments[i].endDate { + let startDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: Double(timeStart + (60 * 5))) + targetScheduleData.append(startDot) + + let endDate = targetSegments[i].endDate + let endDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: endDate) + targetScheduleData.append(endDot) + firstPass = false + } + } + } + + if Storage.shared.graphTargetLine.value { + updateTargetScheduledGraph() + } + } +} From 663fc77f44f84f951698ffdbe1fb8b56c1dd46bf Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:53:38 +0200 Subject: [PATCH 02/11] Add targetGraphStruct for NS target data Added targetGraphStruct to represent target data. --- LoopFollow/Controllers/NightScout.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index 90e3c1e9c..c39509d2e 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -30,6 +30,12 @@ extension MainViewController { var date: TimeInterval } + // NS Target Data Struct + struct targetGraphStruct: Codable { + var targetHigh: Double + var date: TimeInterval + } + // NS Bolus Data Struct struct bolusGraphStruct: Codable { var value: Double From c28ef8335ed5d2189e100c6cf00060eac3e66fee Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:54:33 +0200 Subject: [PATCH 03/11] Add targetProfileSegment struct for Codable support --- LoopFollow/Helpers/DataStructs.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/LoopFollow/Helpers/DataStructs.swift b/LoopFollow/Helpers/DataStructs.swift index 9cd8a175f..10400f7b4 100644 --- a/LoopFollow/Helpers/DataStructs.swift +++ b/LoopFollow/Helpers/DataStructs.swift @@ -17,6 +17,12 @@ class DataStructs { var endDate: TimeInterval } + struct targetProfileSegment: Codable { + var targetHigh: Double + var startDate: TimeInterval + var endDate: TimeInterval + } + // NS Timestamp Only Data Struct struct timestampOnlyStruct: Codable { var date: TimeInterval From e5be26242ff6664e1e6713c8b74bdcbff28b5f70 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:55:24 +0200 Subject: [PATCH 04/11] Add targetScheduleData variable to MainViewController --- LoopFollow/ViewControllers/MainViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 06c5e1f76..3fe44edb7 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -74,6 +74,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { var basalProfile: [basalProfileStruct] = [] var basalData: [basalGraphStruct] = [] var basalScheduleData: [basalGraphStruct] = [] + var targetScheduleData: [targetGraphStruct] = [] var bolusData: [bolusGraphStruct] = [] var smbData: [bolusGraphStruct] = [] var carbData: [carbGraphStruct] = [] From 1c435ccfb10dd8d7ec34ca74f1a7c3ee2e903612 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:56:28 +0200 Subject: [PATCH 05/11] Add updateTargetScheduledGraph method --- LoopFollow/Charts/BGChartStubs.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/Charts/BGChartStubs.swift b/LoopFollow/Charts/BGChartStubs.swift index 700821a66..80f6599f9 100644 --- a/LoopFollow/Charts/BGChartStubs.swift +++ b/LoopFollow/Charts/BGChartStubs.swift @@ -192,6 +192,7 @@ extension MainViewController { func updateBolusGraph() { chartModel.rebuild() } func updateCarbGraph() { chartModel.rebuild() } func updateBasalScheduledGraph() { chartModel.rebuild() } + func updateTargetScheduledGraph() { chartModel.rebuild() } func updateOverrideGraph() { chartModel.rebuild() } func updateBGCheckGraph() { chartModel.rebuild() } func updateSuspendGraph() { chartModel.rebuild() } From fa867a2afdf64582453d80cb80bd35fccffca3f2 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:57:09 +0200 Subject: [PATCH 06/11] Fix indentation and formatting in Profile.swift --- LoopFollow/Controllers/Nightscout/Profile.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index d88c453fa..9c617b7b1 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -123,8 +123,10 @@ extension MainViewController { } } - if Storage.shared.graphBasal.value { + if Storage.shared.graphBasal.value { updateBasalScheduledGraph() } + + updateTargetScheduleData() } } From 9c6c27d33ff179d612672faae93968ca536794c5 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:58:20 +0200 Subject: [PATCH 07/11] Add graphTargetLine storage value --- LoopFollow/Storage/Storage.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index dd63f6dd6..de592345f 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -168,6 +168,7 @@ class Storage { var downloadPrediction = StorageValue(key: "downloadPrediction", defaultValue: true) var graphOtherTreatments = StorageValue(key: "graphOtherTreatments", defaultValue: true) var graphBasal = StorageValue(key: "graphBasal", defaultValue: true) + var graphTargetLine = StorageValue(key: "graphTargetLine", defaultValue: true) var graphBolus = StorageValue(key: "graphBolus", defaultValue: true) var graphCarbs = StorageValue(key: "graphCarbs", defaultValue: true) var bgUpdateDelay = StorageValue(key: "bgUpdateDelay", defaultValue: 10) From dc2ddd0120bda50894aea0028fabbb5e3b88e879 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:59:43 +0200 Subject: [PATCH 08/11] Add graphTargetLine property to AdvancedSettingsViewModel --- LoopFollow/Settings/AdvancedSettingsViewModel.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/LoopFollow/Settings/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 078e62288..9fffec04f 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -22,6 +22,13 @@ class AdvancedSettingsViewModel: ObservableObject { Observable.shared.chartSettingsChanged.value = true } } + + @Published var graphTargetLine: Bool { + didSet { + Storage.shared.graphTargetLine.value = graphTargetLine + Observable.shared.chartSettingsChanged.value = true + } + } @Published var graphBolus: Bool { didSet { @@ -60,6 +67,7 @@ class AdvancedSettingsViewModel: ObservableObject { downloadTreatments = Storage.shared.downloadTreatments.value downloadPrediction = Storage.shared.downloadPrediction.value graphBasal = Storage.shared.graphBasal.value + graphTargetLine = Storage.shared.graphTargetLine.value graphBolus = Storage.shared.graphBolus.value graphCarbs = Storage.shared.graphCarbs.value graphOtherTreatments = Storage.shared.graphOtherTreatments.value From 5b2a4b59edc62ba8e5d613c18804bbb1b4ebc71a Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:00:26 +0200 Subject: [PATCH 09/11] Add toggle for Graph Target Line in settings --- LoopFollow/Settings/AdvancedSettingsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 9665df882..49a7375e5 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -12,6 +12,7 @@ struct AdvancedSettingsView: View { Toggle("Download Treatments", isOn: $viewModel.downloadTreatments) Toggle("Download Prediction", isOn: $viewModel.downloadPrediction) Toggle("Graph Basal", isOn: $viewModel.graphBasal) + Toggle("Graph Target Line", isOn: $viewModel.graphTargetLine) Toggle("Graph Bolus", isOn: $viewModel.graphBolus) Toggle("Graph Carbs", isOn: $viewModel.graphCarbs) Toggle("Graph Other Treatments", isOn: $viewModel.graphOtherTreatments) From 4c8dcfff0c8a1642bcb3fe37882a2601d6764257 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:03:28 +0200 Subject: [PATCH 10/11] Add ScheduledTargetPoint struct and update targetScheduled --- LoopFollow/Charts/BGChartModel.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift index 8acc2859c..91d4d3521 100644 --- a/LoopFollow/Charts/BGChartModel.swift +++ b/LoopFollow/Charts/BGChartModel.swift @@ -82,6 +82,12 @@ final class BGChartModel: ObservableObject { var id: TimeInterval { date.timeIntervalSince1970 } } + struct ScheduledTargetPoint: Identifiable { + let date: Date + let value: Double + var id: TimeInterval { date.timeIntervalSince1970 } + } + struct BandRect: Identifiable { let start: Date let end: Date @@ -131,6 +137,7 @@ final class BGChartModel: ObservableObject { @Published var basal: [BasalStep] = [] @Published var basalScheduled: [ScheduledBasalPoint] = [] + @Published var targetScheduled: [ScheduledTargetPoint] = [] @Published var boluses: [TreatmentPoint] = [] @Published var carbs: [TreatmentPoint] = [] @@ -421,6 +428,7 @@ final class BGChartModel: ObservableObject { // collect the data regardless (it also feeds the info rows), so hidden // kinds are dropped here at render time. let showBasal = Storage.shared.graphBasal.value + let showTargetLine = Storage.shared.graphTargetLine.value let showBolus = Storage.shared.graphBolus.value let showCarbs = Storage.shared.graphCarbs.value let showOtherTreatments = Storage.shared.graphOtherTreatments.value @@ -520,6 +528,10 @@ final class BGChartModel: ObservableObject { basalScheduled = (showBasal ? vc.basalScheduleData : []).map { ScheduledBasalPoint(date: Date(timeIntervalSince1970: $0.date), rate: $0.basalRate) } + + targetScheduled = (showTargetLine ? vc.targetScheduleData : []).map { + ScheduledTargetPoint(date: Date(timeIntervalSince1970: $0.date), value: $0.targetHigh) + } var steps: [BasalStep] = [] let sortedBasal = (showBasal ? vc.basalData : []).sorted { $0.date < $1.date } From 81a1bfe8c326541b4020a7399a8b7a8da553bd82 Mon Sep 17 00:00:00 2001 From: kristinkand <163904051+kristinkand@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:09:42 +0200 Subject: [PATCH 11/11] Implement scheduled target marks in BGChartView Added scheduled target marks to the chart view. --- LoopFollow/Charts/BGChartView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/LoopFollow/Charts/BGChartView.swift b/LoopFollow/Charts/BGChartView.swift index 74f9ec03e..abcba75f6 100644 --- a/LoopFollow/Charts/BGChartView.swift +++ b/LoopFollow/Charts/BGChartView.swift @@ -1042,6 +1042,7 @@ private struct BGChartCanvas: View, Equatable { bgBandMarks basalMarks scheduledBasalMarks + scheduledTargetMarks } coneMarks if !isSmall { @@ -1209,6 +1210,19 @@ private struct BGChartCanvas: View, Equatable { .foregroundStyle(Color.blue.opacity(0.8)) } } + + @ChartContentBuilder + private var scheduledTargetMarks: some ChartContent { + ForEach(windowedLine(model.targetScheduled) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("target", pt.value), + series: .value("series", "targetScheduled") + ) + .lineStyle(StrokeStyle(lineWidth: 2, dash: [10, 5])) + .foregroundStyle(Color.green.opacity(0.8)) + } + } @ChartContentBuilder private var coneMarks: some ChartContent {