From f4f3c4d03d31957387033cc56d40c46d993168fa Mon Sep 17 00:00:00 2001
From: Nathaniel Hamming <nate@hmtconsulting.org>
Date: Mon, 25 Sep 2023 15:25:53 -0300
Subject: [PATCH 01/21] [COASTAL-1291] plugin identifier is no longer class
 property (#92)

---
 TidepoolServiceKit/TidepoolService.swift | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 00332ab..61ce54c 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -32,8 +32,10 @@ public protocol SessionStorage {
 }
 
 public final class TidepoolService: Service, TAPIObserver, ObservableObject {
-
-    public static let pluginIdentifier = "TidepoolService"
+    
+    public static let serviceIdentifier: String = "TidepoolService"
+    
+    public var pluginIdentifier: String { Self.serviceIdentifier }
 
     public static let localizedTitle = LocalizedString("Tidepool", comment: "The title of the Tidepool service")
 
@@ -66,7 +68,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     private var hostIdentifier: String?
     private var hostVersion: String?
 
-    private let log = OSLog(category: pluginIdentifier)
+    private let log = OSLog(category: "TidepoolService")
     private let tidepoolKitLog = OSLog(category: "TidepoolKit")
 
     public init(hostIdentifier: String, hostVersion: String) {

From 9975401517880a1d95660b7bdaed2e12c83b314a Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Tue, 26 Sep 2023 11:16:08 -0500
Subject: [PATCH 02/21] Merge fixes

---
 TidepoolServiceKit/TidepoolService.swift | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 805eaa1..ab60271 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -33,7 +33,9 @@ public protocol SessionStorage {
 
 public final class TidepoolService: Service, TAPIObserver, ObservableObject {
 
-    public static let pluginIdentifier = "TidepoolService"
+    public static let serviceIdentifier: String = "TidepoolService"
+
+    public var pluginIdentifier: String { Self.serviceIdentifier }
 
     public static let localizedTitle = LocalizedString("Tidepool", comment: "The title of the Tidepool service")
 
@@ -66,7 +68,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     private var hostIdentifier: String?
     private var hostVersion: String?
 
-    private let log = OSLog(category: pluginIdentifier)
+    private let log = OSLog(category: "TidepoolService")
     private let tidepoolKitLog = OSLog(category: "TidepoolKit")
 
     public init(hostIdentifier: String, hostVersion: String) {

From 6cf2c2202a88e694f6cd2e62a4789f67913a6138 Mon Sep 17 00:00:00 2001
From: Nathaniel Hamming <nate@hmtconsulting.org>
Date: Tue, 10 Oct 2023 15:43:52 -0300
Subject: [PATCH 03/21] added testflight configuration (#94)

---
 TidepoolService.xcodeproj/project.pbxproj | 235 +++++++++++++++++++++-
 1 file changed, 234 insertions(+), 1 deletion(-)

diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj
index be8ee8e..82cf48e 100644
--- a/TidepoolService.xcodeproj/project.pbxproj
+++ b/TidepoolService.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 52;
+	objectVersion = 54;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -1354,6 +1354,233 @@
 			};
 			name = Release;
 		};
+		B4E7CFA82AD03299009B4DF2 /* Testflight */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_GCD_PERFORMANCE = YES;
+				CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
+				CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
+				CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_ASSIGN_ENUM = YES;
+				CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES_ERROR;
+				CLANG_WARN_BOOL_CONVERSION = YES_ERROR;
+				CLANG_WARN_COMMA = YES_ERROR;
+				CLANG_WARN_CONSTANT_CONVERSION = YES_ERROR;
+				CLANG_WARN_CXX0X_EXTENSIONS = YES;
+				CLANG_WARN_DELETE_NON_VIRTUAL_DTOR = YES_ERROR;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES_ERROR;
+				CLANG_WARN_FLOAT_CONVERSION = YES_ERROR;
+				CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES_ERROR;
+				CLANG_WARN_MISSING_NOESCAPE = YES_ERROR;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR;
+				CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES;
+				CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_INTERFACE_IVARS = YES_ERROR;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR;
+				CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES;
+				CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES_AGGRESSIVE;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_PRAGMA_PACK = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;
+				CLANG_WARN_VEXING_PARSE = YES_ERROR;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES;
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
+				GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES_ERROR;
+				GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
+				GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
+				GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
+				GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES;
+				GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES;
+				GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
+				GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
+				GCC_WARN_SHADOW = YES;
+				GCC_WARN_SIGN_COMPARE = YES;
+				GCC_WARN_STRICT_SELECTOR_MATCH = YES;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNKNOWN_PRAGMAS = YES;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_LABEL = YES;
+				GCC_WARN_UNUSED_PARAMETER = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+				LOCALIZED_STRING_MACRO_NAMES = (
+					NSLocalizedString,
+					CFLocalizedString,
+					LocalizedString,
+				);
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+				WARNING_CFLAGS = "-Wall";
+			};
+			name = Testflight;
+		};
+		B4E7CFA92AD03299009B4DF2 /* Testflight */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CODE_SIGN_IDENTITY = "";
+				CODE_SIGN_STYLE = Automatic;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = "";
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				FRAMEWORK_SEARCH_PATHS = "$(inherited)";
+				INFOPLIST_FILE = TidepoolServiceKit/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKit;
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SKIP_INSTALL = YES;
+				SUPPORTS_MACCATALYST = NO;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Testflight;
+		};
+		B4E7CFAA2AD03299009B4DF2 /* Testflight */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = "";
+				INFOPLIST_FILE = TidepoolServiceKitTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Testflight;
+		};
+		B4E7CFAB2AD03299009B4DF2 /* Testflight */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CODE_SIGN_IDENTITY = "";
+				CODE_SIGN_STYLE = Automatic;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = "";
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				ENABLE_PREVIEWS = YES;
+				INFOPLIST_FILE = TidepoolServiceKitUI/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitUI;
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SKIP_INSTALL = YES;
+				SUPPORTS_MACCATALYST = NO;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Testflight;
+		};
+		B4E7CFAC2AD03299009B4DF2 /* Testflight */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = "";
+				INFOPLIST_FILE = TidepoolServiceKitUITests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Testflight;
+		};
+		B4E7CFAD2AD03299009B4DF2 /* Testflight */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CODE_SIGN_IDENTITY = "";
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = "";
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				INFOPLIST_FILE = TidepoolServiceKitPlugin/Info.plist;
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitPlugin;
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SKIP_INSTALL = YES;
+				SUPPORTS_MACCATALYST = NO;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				WRAPPER_EXTENSION = loopplugin;
+			};
+			name = Testflight;
+		};
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
@@ -1361,6 +1588,7 @@
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				A94AE4EA235A89B5005CA320 /* Debug */,
+				B4E7CFAD2AD03299009B4DF2 /* Testflight */,
 				A94AE4EB235A89B5005CA320 /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
@@ -1370,6 +1598,7 @@
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				A9DAACF122E7978800E76C9F /* Debug */,
+				B4E7CFA82AD03299009B4DF2 /* Testflight */,
 				A9DAACF222E7978800E76C9F /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
@@ -1379,6 +1608,7 @@
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				A9DAAD1122E7987800E76C9F /* Debug */,
+				B4E7CFA92AD03299009B4DF2 /* Testflight */,
 				A9DAAD1222E7987800E76C9F /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
@@ -1388,6 +1618,7 @@
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				A9DAAD1422E7987800E76C9F /* Debug */,
+				B4E7CFAA2AD03299009B4DF2 /* Testflight */,
 				A9DAAD1522E7987800E76C9F /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
@@ -1397,6 +1628,7 @@
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				A9DAAD2D22E7988900E76C9F /* Debug */,
+				B4E7CFAB2AD03299009B4DF2 /* Testflight */,
 				A9DAAD2E22E7988900E76C9F /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;
@@ -1406,6 +1638,7 @@
 			isa = XCConfigurationList;
 			buildConfigurations = (
 				A9DAAD3022E7988900E76C9F /* Debug */,
+				B4E7CFAC2AD03299009B4DF2 /* Testflight */,
 				A9DAAD3122E7988900E76C9F /* Release */,
 			);
 			defaultConfigurationIsVisible = 0;

From 103fb13296ea113e799010f987ede54661194638 Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Tue, 24 Oct 2023 10:42:17 -0500
Subject: [PATCH 04/21] Update test for api change

---
 .../Extensions/StoredDosingDecisionTests.swift                   | 1 -
 1 file changed, 1 deletion(-)

diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
index 78ca2ed..082152a 100644
--- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
+++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
@@ -354,7 +354,6 @@ fileprivate extension StoredDosingDecision {
                                                               duration: .minutes(30))
         let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, bolusUnits: 1.25)
         let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 1.2,
-                                                                                                                    pendingInsulin: 0.75,
                                                                                                                     notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!,
                                                                                                                                                                                            quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))),
                                                                           date: dateFormatter.date(from: "2020-05-14T22:38:16Z")!)

From 9fd01cf9000fccab79f3374c508aafccd5d3ee55 Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Tue, 19 Dec 2023 08:49:40 -0600
Subject: [PATCH 05/21] Temporary preset activations moved out of LoopSettings.
 (#96)

---
 .../Extensions/StoredSettings.swift           |  41 +------
 TidepoolServiceKit/TidepoolService.swift      | 116 +++++-------------
 .../Extensions/StoredSettingsTests.swift      |  38 ------
 .../TidepoolServiceTests.swift                |  78 +-----------
 4 files changed, 36 insertions(+), 237 deletions(-)

diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift
index e3993b6..9e212cb 100644
--- a/TidepoolServiceKit/Extensions/StoredSettings.swift
+++ b/TidepoolServiceKit/Extensions/StoredSettings.swift
@@ -21,8 +21,6 @@ import TidepoolKit
  - preMealTargetRange               ClosedRange<HKQuantity>?                TPumpSettingsDatum.bloodGlucoseTargetPreprandial
  - workoutTargetRange               ClosedRange<HKQuantity>?                TPumpSettingsDatum.bloodGlucoseTargetPhysicalActivity
  - overridePresets                  [TemporaryScheduleOverridePreset]?      TPumpSettingsDatum.overridePresets
- - scheduleOverride                 TemporaryScheduleOverride?              TPumpSettingsOverrideDeviceEventDatum.*
- - preMealOverride                  TemporaryScheduleOverride?              TPumpSettingsOverrideDeviceEventDatum.*
  - maximumBasalRatePerHour          Double?                                 TPumpSettingsDatum.basal.rateMaximum.value
  - maximumBolus                     Double?                                 TPumpSettingsDatum.bolus.amountMaximum.value
  - suspendThreshold                 GlucoseThreshold?                       TPumpSettingsDatum.bloodGlucoseSafetyLimit
@@ -38,7 +36,6 @@ import TidepoolKit
  - syncIdentifier                   UUID                                    .id, .origin, .payload["syncIdentifier"]
  
  Notes:
- - The active override (scheduleOverride or preMealOverride) are stored in TPumpSettingsOverrideDeviceEventDatum.
  - Assumes same time zone for basalRateSchedule, glucoseTargetRangeSchedule, carbRatioSchedule, insulinSensitivitySchedule.
  - StoredSettings.notificationSettings.carPlaySetting is not included as it is unneeded by backend.
  - StoredSettings.notificationSettings.showPreviewsSetting is not included as it is unneeded by backend.
@@ -116,29 +113,6 @@ extension StoredSettings: IdentifiableDatum {
                                origin: origin)
     }
     
-    func datumPumpSettingsOverrideDeviceEvent(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpSettingsOverrideDeviceEventDatum? {
-        guard let activeOverride = activeOverride else {
-            return nil
-        }
-        let datum = TPumpSettingsOverrideDeviceEventDatum(time: activeOverride.datumTime,
-                                                          overrideType: activeOverride.datumOverrideType,
-                                                          overridePreset: activeOverride.datumOverridePreset,
-                                                          method: activeOverride.datumMethod,
-                                                          duration: activeOverride.datumDuration,
-                                                          expectedDuration: activeOverride.datumExpectedDuration,
-                                                          bloodGlucoseTarget: activeOverride.datumBloodGlucoseTarget,
-                                                          basalRateScaleFactor: activeOverride.datumBasalRateScaleFactor,
-                                                          carbohydrateRatioScaleFactor: activeOverride.datumCarbohydrateRatioScaleFactor,
-                                                          insulinSensitivityScaleFactor: activeOverride.datumInsulinSensitivityScaleFactor,
-                                                          units: activeOverride.datumUnits)
-        let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion)
-        return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self),
-                               timeZone: datumTimeZone,
-                               timeZoneOffset: datumTimeZoneOffset,
-                               payload: datumPayload,
-                               origin: origin)
-    }
-
     var syncIdentifierAsString: String { syncIdentifier.uuidString }
 
     private var datumTime: Date { date }
@@ -304,7 +278,7 @@ extension StoredSettings: IdentifiableDatum {
     private var datumPumpName: String? { pumpDevice?.name }
 
     private var datumPumpOverridePresets: [String: TPumpSettingsDatum.OverridePreset]? {
-        guard let overridePresets = overridePresets, !overridePresets.isEmpty else {
+        guard !overridePresets.isEmpty else {
             return nil
         }
         return overridePresets.reduce(into: [:]) { $0[$1.name] = $1.datum }
@@ -333,19 +307,6 @@ extension StoredSettings: IdentifiableDatum {
         return dictionary
     }
     
-    private var activeOverride: TemporaryScheduleOverride? {
-        switch (preMealOverride, scheduleOverride) {
-        case (let preMealOverride?, nil):
-            return preMealOverride
-        case (nil, let scheduleOverride?):
-            return scheduleOverride
-        case (let preMealOverride?, let scheduleOverride?):
-            return preMealOverride.scheduledEndDate > date ? preMealOverride : scheduleOverride
-        case (nil, nil):
-            return nil
-        }
-    }
-
     public static var activeScheduleNameDefault: String { "Default" }
 }
 
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index f7590b6..ba7ea50 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -63,8 +63,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
 
     private var lastPumpSettingsDatum: TPumpSettingsDatum?
 
-    private var lastPumpSettingsOverrideDeviceEventDatum: TPumpSettingsOverrideDeviceEventDatum?
-
     private var hostIdentifier: String?
     private var hostVersion: String?
 
@@ -95,7 +93,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
             self.lastControllerSettingsDatum = (rawState["lastControllerSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TControllerSettingsDatum.self, from: $0) }
             self.lastCGMSettingsDatum = (rawState["lastCGMSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TCGMSettingsDatum.self, from: $0) }
             self.lastPumpSettingsDatum = (rawState["lastPumpSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TPumpSettingsDatum.self, from: $0) }
-            self.lastPumpSettingsOverrideDeviceEventDatum = (rawState["lastPumpSettingsOverrideDeviceEventDatum"] as? Data).flatMap { try? Self.decoder.decode(TPumpSettingsOverrideDeviceEventDatum.self, from: $0) }
             self.session = try sessionStorage.getSession(for: sessionService)
             Task {
                 await tapi.setSession(session)
@@ -118,7 +115,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
         rawValue["lastControllerSettingsDatum"] = lastControllerSettingsDatum.flatMap { try? Self.encoder.encode($0) }
         rawValue["lastCGMSettingsDatum"] = lastCGMSettingsDatum.flatMap { try? Self.encoder.encode($0) }
         rawValue["lastPumpSettingsDatum"] = lastPumpSettingsDatum.flatMap { try? Self.encoder.encode($0) }
-        rawValue["lastPumpSettingsOverrideDeviceEventDatum"] = lastPumpSettingsOverrideDeviceEventDatum.flatMap { try? Self.encoder.encode($0) }
         return rawValue
     }
 
@@ -280,7 +276,32 @@ extension TidepoolService: TLogging {
 extension TidepoolService: RemoteDataService {
 
     public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride], completion: @escaping (Result<Bool, Error>) -> Void) {
-        // TODO: Implement
+        // TODO: https://tidepool.atlassian.net/browse/LOOP-4769
+
+        // The following code is taken from previous upload code when override events where stored in settings
+        // To be implemented with
+
+        //        guard let activeOverride = activeOverride else {
+        //            return nil
+        //        }
+        //        let datum = TPumpSettingsOverrideDeviceEventDatum(time: activeOverride.datumTime,
+        //                                                          overrideType: activeOverride.datumOverrideType,
+        //                                                          overridePreset: activeOverride.datumOverridePreset,
+        //                                                          method: activeOverride.datumMethod,
+        //                                                          duration: activeOverride.datumDuration,
+        //                                                          expectedDuration: activeOverride.datumExpectedDuration,
+        //                                                          bloodGlucoseTarget: activeOverride.datumBloodGlucoseTarget,
+        //                                                          basalRateScaleFactor: activeOverride.datumBasalRateScaleFactor,
+        //                                                          carbohydrateRatioScaleFactor: activeOverride.datumCarbohydrateRatioScaleFactor,
+        //                                                          insulinSensitivityScaleFactor: activeOverride.datumInsulinSensitivityScaleFactor,
+        //                                                          units: activeOverride.datumUnits)
+        //        let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+        //        return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self),
+        //                               timeZone: datumTimeZone,
+        //                               timeZoneOffset: datumTimeZoneOffset,
+        //                               payload: datumPayload,
+        //                               origin: origin)
+
         completion(.success(true))
     }
 
@@ -448,7 +469,7 @@ extension TidepoolService: RemoteDataService {
             return
         }
 
-        let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum) = calculateSettingsData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+        let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum) = calculateSettingsData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
 
         Task {
             do {
@@ -457,7 +478,6 @@ extension TidepoolService: RemoteDataService {
                 self.lastControllerSettingsDatum = lastControllerSettingsDatum
                 self.lastCGMSettingsDatum = lastCGMSettingsDatum
                 self.lastPumpSettingsDatum = lastPumpSettingsDatum
-                self.lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum
                 self.completeUpdate()
                 completion(.success(createdUploaded || updatedUploaded))
             } catch {
@@ -466,19 +486,12 @@ extension TidepoolService: RemoteDataService {
         }
     }
 
-    func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?, TPumpSettingsOverrideDeviceEventDatum?) {
+    func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?) {
         var created: [TDatum] = []
         var updated: [TDatum] = []
         var lastControllerSettingsDatum = lastControllerSettingsDatum
         var lastCGMSettingsDatum = lastCGMSettingsDatum
         var lastPumpSettingsDatum = lastPumpSettingsDatum
-        var lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum
-
-        // A StoredSettings can generate a TPumpSettingsDatum and an optional TPumpSettingsOverrideDeviceEventDatum if there is an
-        // enabled override. Only upload the TPumpSettingsDatum or TPumpSettingsOverrideDeviceEventDatum if they have CHANGED.
-        // If the TPumpSettingsOverrideDeviceEventDatum has changed, then also re-upload the previous uploaded
-        // TPumpSettingsOverrideDeviceEventDatum with an updated duration and potentially expected duration, but only if the
-        // duration is calculated to be ended early.
 
         stored.forEach {
 
@@ -493,15 +506,11 @@ extension TidepoolService: RemoteDataService {
             let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
             let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum)
 
-            let pumpSettingsOverrideDeviceEventDatum = $0.datumPumpSettingsOverrideDeviceEvent(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
-            let pumpSettingsOverrideDeviceEventDatumIsEffectivelyEquivalent = TPumpSettingsOverrideDeviceEventDatum.areEffectivelyEquivalent(old: lastPumpSettingsOverrideDeviceEventDatum, new: pumpSettingsOverrideDeviceEventDatum)
-
             // Associate the data
 
             var controllerSettingsAssociations: [TAssociation] = []
             var cgmSettingsAssociations: [TAssociation] = []
             var pumpSettingsAssociations: [TAssociation] = []
-            var pumpSettingsOverrideDeviceEventAssociations: [TAssociation] = []
 
             if let controllerSettingsDatum = controllerSettingsDatumIsEffectivelyEquivalent ? lastControllerSettingsDatum : controllerSettingsDatum {
                 let association = TAssociation(type: .datum, id: controllerSettingsDatum.id!, reason: "controllerSettings")
@@ -517,13 +526,11 @@ extension TidepoolService: RemoteDataService {
                 let association = TAssociation(type: .datum, id: pumpSettingsDatum.id!, reason: "pumpSettings")
                 controllerSettingsAssociations.append(association)
                 cgmSettingsAssociations.append(association)
-                pumpSettingsOverrideDeviceEventAssociations.append(association)
             }
 
             controllerSettingsDatum.append(associations: controllerSettingsAssociations)
             cgmSettingsDatum.append(associations: cgmSettingsAssociations)
             pumpSettingsDatum.append(associations: pumpSettingsAssociations)
-            pumpSettingsOverrideDeviceEventDatum?.append(associations: pumpSettingsOverrideDeviceEventAssociations)
 
             // Upload and update the data, if necessary
 
@@ -541,27 +548,9 @@ extension TidepoolService: RemoteDataService {
                 created.append(pumpSettingsDatum)
                 lastPumpSettingsDatum = pumpSettingsDatum
             }
-
-            if !pumpSettingsOverrideDeviceEventDatumIsEffectivelyEquivalent {
-
-                // If we need to update the duration of the last override, then do so
-                if let lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum,
-                   lastPumpSettingsOverrideDeviceEventDatum.updateDuration(basedUpon: pumpSettingsOverrideDeviceEventDatum?.time ?? pumpSettingsDatum.time) {
-
-                    // If it isn't already being created, then update it
-                    if !created.contains(where: { $0 === lastPumpSettingsOverrideDeviceEventDatum }) {
-                        updated.append(lastPumpSettingsOverrideDeviceEventDatum)
-                    }
-                }
-
-                if let pumpSettingsOverrideDeviceEventDatum = pumpSettingsOverrideDeviceEventDatum {
-                    created.append(pumpSettingsOverrideDeviceEventDatum)
-                }
-                lastPumpSettingsOverrideDeviceEventDatum = pumpSettingsOverrideDeviceEventDatum
-            }
         }
 
-        return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum)
+        return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum)
     }
 
     private func createData(_ data: [TDatum]) async throws -> Bool {
@@ -782,53 +771,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
     }
 }
 
-extension TPumpSettingsOverrideDeviceEventDatum: EffectivelyEquivalent {
-
-    // All TDatum properties can be ignored EXCEPT time for this datum type
-    // Time is gather from the actual scheduled override and NOT the StoredSettings so it is valid and necessary for comparison
-    func isEffectivelyEquivalent(to other: TPumpSettingsOverrideDeviceEventDatum) -> Bool {
-        return self.time == other.time &&
-            self.overrideType == other.overrideType &&
-            self.overridePreset == other.overridePreset &&
-            self.method == other.method &&
-            self.duration == other.duration &&
-            self.expectedDuration == other.expectedDuration &&
-            self.bloodGlucoseTarget == other.bloodGlucoseTarget &&
-            self.basalRateScaleFactor == other.basalRateScaleFactor &&
-            self.carbohydrateRatioScaleFactor == other.carbohydrateRatioScaleFactor &&
-            self.insulinSensitivityScaleFactor == other.insulinSensitivityScaleFactor &&
-            self.units == other.units
-    }
-
-    var isEffectivelyEmpty: Bool {
-        return overrideType == nil &&
-            overridePreset == nil &&
-            method == nil &&
-            duration == nil &&
-            expectedDuration == nil &&
-            bloodGlucoseTarget == nil &&
-            basalRateScaleFactor == nil &&
-            carbohydrateRatioScaleFactor == nil &&
-            insulinSensitivityScaleFactor == nil &&
-            units == nil
-    }
-
-    func updateDuration(basedUpon endTime: Date?) -> Bool {
-        guard let endTime = endTime, let time = time, endTime > time else {
-            return false
-        }
-
-        let updatedDuration = time.distance(to: endTime)
-        guard duration == nil || updatedDuration < duration! else {
-            return false
-        }
-
-        self.expectedDuration = duration
-        self.duration = updatedDuration
-        return true
-    }
-}
-
 fileprivate extension TDosingDecisionDatum {
 
     // Ignore reason and units as they are always specified
diff --git a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift
index c08ce84..8e33ddf 100644
--- a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift
+++ b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift
@@ -247,42 +247,6 @@ class StoredSettingsTests: XCTestCase {
         )
     }
     
-    func testDatumPumpSettingsOverrideDeviceEvent() {
-        let data = try! Self.encoder.encode(StoredSettings.test.datumPumpSettingsOverrideDeviceEvent(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3"))
-        XCTAssertEqual(String(data: data, encoding: .utf8), """
-{
-  "basalRateScaleFactor" : 0.5,
-  "bgTarget" : {
-    "high" : 90,
-    "low" : 80
-  },
-  "carbRatioScaleFactor" : 2,
-  "id" : "f89ad59a42430ab89dd2eab3a3e4df84",
-  "insulinSensitivityScaleFactor" : 2,
-  "method" : "manual",
-  "origin" : {
-    "id" : "2A67A303-1234-4CB8-1234-79498265368E:deviceEvent/pumpSettingsOverride",
-    "name" : "Loop",
-    "type" : "application",
-    "version" : "1.2.3"
-  },
-  "overrideType" : "preprandial",
-  "payload" : {
-    "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E"
-  },
-  "subType" : "pumpSettingsOverride",
-  "time" : "2020-05-14T14:38:39.000Z",
-  "timezone" : "America/Los_Angeles",
-  "timezoneOffset" : -420,
-  "type" : "deviceEvent",
-  "units" : {
-    "bg" : "mg/dL"
-  }
-}
-"""
-        )
-    }
-    
     private static let encoder: JSONEncoder = {
         let encoder = JSONEncoder.tidepool
         encoder.outputFormatting.insert(.prettyPrinted)
@@ -392,8 +356,6 @@ fileprivate extension StoredSettings {
                               preMealTargetRange: preMealTargetRange,
                               workoutTargetRange: workoutTargetRange,
                               overridePresets: overridePresets,
-                              scheduleOverride: scheduleOverride,
-                              preMealOverride: preMealOverride,
                               maximumBasalRatePerHour: maximumBasalRatePerHour,
                               maximumBolus: maximumBolus,
                               suspendThreshold: suspendThreshold,
diff --git a/TidepoolServiceKitTests/TidepoolServiceTests.swift b/TidepoolServiceKitTests/TidepoolServiceTests.swift
index 7ace7f1..0d8d70d 100644
--- a/TidepoolServiceKitTests/TidepoolServiceTests.swift
+++ b/TidepoolServiceKitTests/TidepoolServiceTests.swift
@@ -52,7 +52,7 @@ class TidepoolServiceTests: XCTestCase {
         let settings = [StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"),
                                        cgmDevice: HKDevice(name: "CGM #1"),
                                        pumpDevice: HKDevice(name: "Pump #1"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
+        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
         XCTAssertEqual(created.count, 3)
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1")
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).associations!.count, 2)
@@ -70,7 +70,6 @@ class TidepoolServiceTests: XCTestCase {
         XCTAssertEqual(lastControllerSettings!.id, created[0].id)
         XCTAssertEqual(lastCGMSettings!.id, created[1].id)
         XCTAssertEqual(lastPumpSettings!.id, created[2].id)
-        XCTAssertNil(lastPumpSettingsOverride)
     }
     
     func testCalculateSettingsDataUpdateControllerSettings() {
@@ -80,7 +79,7 @@ class TidepoolServiceTests: XCTestCase {
                         StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"),
                                        cgmDevice: HKDevice(name: "CGM #1"),
                                        pumpDevice: HKDevice(name: "Pump #1"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
+        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
         XCTAssertEqual(created.count, 4)
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1")
         XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1")
@@ -93,7 +92,6 @@ class TidepoolServiceTests: XCTestCase {
         XCTAssertEqual(lastControllerSettings!.id, created[3].id)
         XCTAssertEqual(lastCGMSettings!.id, created[1].id)
         XCTAssertEqual(lastPumpSettings!.id, created[2].id)
-        XCTAssertNil(lastPumpSettingsOverride)
     }
     
     func testCalculateSettingsDataUpdateCGMSettings() {
@@ -103,7 +101,7 @@ class TidepoolServiceTests: XCTestCase {
                         StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"),
                                        cgmDevice: HKDevice(name: "CGM #2"),
                                        pumpDevice: HKDevice(name: "Pump #1"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
+        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
         XCTAssertEqual(created.count, 4)
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1")
         XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1")
@@ -116,7 +114,6 @@ class TidepoolServiceTests: XCTestCase {
         XCTAssertEqual(lastControllerSettings!.id, created[0].id)
         XCTAssertEqual(lastCGMSettings!.id, created[3].id)
         XCTAssertEqual(lastPumpSettings!.id, created[2].id)
-        XCTAssertNil(lastPumpSettingsOverride)
     }
     
     func testCalculateSettingsDataUpdatePumpSettings() {
@@ -126,7 +123,7 @@ class TidepoolServiceTests: XCTestCase {
                         StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"),
                                        cgmDevice: HKDevice(name: "CGM #1"),
                                        pumpDevice: HKDevice(name: "Pump #2"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
+        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
         XCTAssertEqual(created.count, 4)
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1")
         XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1")
@@ -139,7 +136,6 @@ class TidepoolServiceTests: XCTestCase {
         XCTAssertEqual(lastControllerSettings!.id, created[0].id)
         XCTAssertEqual(lastCGMSettings!.id, created[1].id)
         XCTAssertEqual(lastPumpSettings!.id, created[3].id)
-        XCTAssertNil(lastPumpSettingsOverride)
     }
     
     func testCalculateSettingsDataUpdateMultipleOne() {
@@ -149,7 +145,7 @@ class TidepoolServiceTests: XCTestCase {
                         StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"),
                                        cgmDevice: HKDevice(name: "CGM #2"),
                                        pumpDevice: HKDevice(name: "Pump #2"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
+        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
         XCTAssertEqual(created.count, 6)
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1")
         XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1")
@@ -170,7 +166,6 @@ class TidepoolServiceTests: XCTestCase {
         XCTAssertEqual(lastControllerSettings!.id, created[3].id)
         XCTAssertEqual(lastCGMSettings!.id, created[4].id)
         XCTAssertEqual(lastPumpSettings!.id, created[5].id)
-        XCTAssertNil(lastPumpSettingsOverride)
     }
     
     func testCalculateSettingsDataUpdateMultipleMultiple() {
@@ -186,7 +181,7 @@ class TidepoolServiceTests: XCTestCase {
                         StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"),
                                        cgmDevice: HKDevice(name: "CGM #2"),
                                        pumpDevice: HKDevice(name: "Pump #2"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
+        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
         XCTAssertEqual(created.count, 6)
         XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1")
         XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1")
@@ -207,69 +202,8 @@ class TidepoolServiceTests: XCTestCase {
         XCTAssertEqual(lastControllerSettings!.id, created[3].id)
         XCTAssertEqual(lastCGMSettings!.id, created[4].id)
         XCTAssertEqual(lastPumpSettings!.id, created[5].id)
-        XCTAssertNil(lastPumpSettingsOverride)
     }
     
-    func testCalculateSettingsDataPumpOverrideSingle() {
-        let scheduleOverride = TemporaryScheduleOverride()
-        let settings = [StoredSettings(pumpDevice: HKDevice(name: "Pump #1")),
-                        StoredSettings(scheduleOverride: scheduleOverride,
-                                       pumpDevice: HKDevice(name: "Pump #1")),
-                        StoredSettings(scheduleOverride: scheduleOverride,
-                                       controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"),
-                                       pumpDevice: HKDevice(name: "Pump #1"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
-        XCTAssertEqual(created.count, 3)
-        XCTAssertEqual((created[0] as! TPumpSettingsDatum).name, "Pump #1")
-        XCTAssertNil((created[0] as! TPumpSettingsDatum).associations)
-        XCTAssertNil((created[1] as! TPumpSettingsOverrideDeviceEventDatum).duration)
-        XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations!.count, 1)
-        XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations![0].id, created[0].id)
-        XCTAssertEqual((created[2] as! TControllerSettingsDatum).device!.name, "Controller #1")
-        XCTAssertTrue(updated.isEmpty)
-        XCTAssertEqual(lastControllerSettings!.id, created[2].id)
-        XCTAssertNil(lastCGMSettings)
-        XCTAssertEqual(lastPumpSettings!.id, created[0].id)
-        XCTAssertEqual(lastPumpSettingsOverride!.id, created[1].id)
-    }
-    
-    func testCalculateSettingsDataPumpOverrideMultipleAllCreated() {
-        let settings = [StoredSettings(pumpDevice: HKDevice(name: "Pump #1")),
-                        StoredSettings(scheduleOverride: TemporaryScheduleOverride(duration: .minutes(30)),
-                                       pumpDevice: HKDevice(name: "Pump #1")),
-                        StoredSettings(preMealOverride: TemporaryScheduleOverride(),
-                                       pumpDevice: HKDevice(name: "Pump #1")),
-                        StoredSettings(pumpDevice: HKDevice(name: "Pump #1")),
-                        StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"),
-                                       pumpDevice: HKDevice(name: "Pump #1"))]
-        let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3")
-        XCTAssertEqual(created.count, 4)
-        XCTAssertEqual((created[0] as! TPumpSettingsDatum).name, "Pump #1")
-        XCTAssertNil((created[0] as! TPumpSettingsDatum).associations)
-        XCTAssertNotNil((created[1] as! TPumpSettingsOverrideDeviceEventDatum).duration)
-        XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations!.count, 1)
-        XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations![0].id, created[0].id)
-        XCTAssertNotNil((created[2] as! TPumpSettingsOverrideDeviceEventDatum).duration)
-        XCTAssertEqual((created[2] as! TPumpSettingsOverrideDeviceEventDatum).associations!.count, 1)
-        XCTAssertEqual((created[2] as! TPumpSettingsOverrideDeviceEventDatum).associations![0].id, created[0].id)
-        XCTAssertEqual((created[3] as! TControllerSettingsDatum).device!.name, "Controller #1")
-        XCTAssertTrue(updated.isEmpty)
-        XCTAssertEqual(lastControllerSettings!.id, created[3].id)
-        XCTAssertNil(lastCGMSettings)
-        XCTAssertEqual(lastPumpSettings!.id, created[0].id)
-        XCTAssertNil(lastPumpSettingsOverride)
-    }
-}
-
-fileprivate extension TemporaryScheduleOverride {
-    init(duration: TimeInterval? = nil) {
-        self.init(context: .custom,
-                  settings: TemporaryScheduleOverrideSettings(targetRange: nil, insulinNeedsScaleFactor: 1.2),
-                  startDate: Date(),
-                  duration: duration != nil ? .finite(duration!) : .indefinite,
-                  enactTrigger: .local,
-                  syncIdentifier: UUID())
-    }
 }
 
 fileprivate extension StoredSettings.ControllerDevice {

From 5d582adebd688cffc6fceca58a87fa44592df5b4 Mon Sep 17 00:00:00 2001
From: Cameron Ingham <camji555@gmail.com>
Date: Tue, 16 Jan 2024 13:53:12 -0800
Subject: [PATCH 06/21] [LOOP-4788] Fix Unit Tests for iOS 17

---
 .../Extensions/StoredDosingDecisionTests.swift                  | 2 +-
 .../Extensions/StoredGlucoseSampleTests.swift                   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
index 082152a..a0e7695 100644
--- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
+++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
@@ -138,7 +138,7 @@ class StoredDosingDecisionTests: XCTestCase {
     "amount" : 1.25
   },
   "requestedBolus" : {
-    "amount" : 0.80000000000000004
+    "amount" : 0.8
   },
   "smbg" : {
     "time" : "2020-05-14T22:09:00.000Z",
diff --git a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift
index 18641d5..ac1c165 100644
--- a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift
+++ b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift
@@ -119,7 +119,7 @@ class StoredGlucoseSampleTests: XCTestCase {
   },
   "time" : "2020-01-02T03:00:23.000Z",
   "trend" : "constant",
-  "trendRate" : 0.10000000000000001,
+  "trendRate" : 0.1,
   "type" : "cbg",
   "units" : "mg/dL",
   "value" : 123

From 2b8dc515f7dd4946c60384dcc9529effc78b4e4d Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Tue, 5 Mar 2024 12:06:27 -0600
Subject: [PATCH 07/21] LOOP-4781 Types moved to LoopAlgorithm (#98)

* Types moved to LoopAlgorithm

* Types moved to LoopAlgorithm
---
 TidepoolServiceKit/Extensions/InsulinType.swift                | 1 +
 .../Extensions/StoredDosingDecisionTests.swift                 | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/TidepoolServiceKit/Extensions/InsulinType.swift b/TidepoolServiceKit/Extensions/InsulinType.swift
index d436afc..1437960 100644
--- a/TidepoolServiceKit/Extensions/InsulinType.swift
+++ b/TidepoolServiceKit/Extensions/InsulinType.swift
@@ -8,6 +8,7 @@
 
 import LoopKit
 import TidepoolKit
+import LoopAlgorithm
 
 extension InsulinType {
     var datum: TInsulinDatum.Formulation {
diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
index a0e7695..668c1cf 100644
--- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
+++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift
@@ -10,6 +10,7 @@ import XCTest
 import HealthKit
 import LoopKit
 import TidepoolKit
+import LoopAlgorithm
 @testable import TidepoolServiceKit
 
 class StoredDosingDecisionTests: XCTestCase {
@@ -354,7 +355,7 @@ fileprivate extension StoredDosingDecision {
                                                               duration: .minutes(30))
         let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, bolusUnits: 1.25)
         let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 1.2,
-                                                                                                                    notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!,
+                                                                                                                    notice: .predictedGlucoseBelowTarget(minGlucose: SimpleGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!,
                                                                                                                                                                                            quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))),
                                                                           date: dateFormatter.date(from: "2020-05-14T22:38:16Z")!)
         let manualBolusRequested = 0.8

From bbc2c544c06d085f809665b14addd12cc8bf5e02 Mon Sep 17 00:00:00 2001
From: Nathaniel Hamming <nate@hmtconsulting.org>
Date: Fri, 7 Jun 2024 14:15:39 -0300
Subject: [PATCH 08/21] [LOOP-4801] adding pump inoperable (#101)

---
 TidepoolServiceKit/Extensions/StoredDosingDecision.swift | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift
index 7710caf..24e3f56 100644
--- a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift
+++ b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift
@@ -298,7 +298,7 @@ fileprivate extension StoredDosingDecision.ControllerStatus.BatteryState {
 }
 
 fileprivate extension PumpManagerStatus.BasalDeliveryState {
-    var datum: TPumpStatusDatum.BasalDelivery {
+    var datum: TPumpStatusDatum.BasalDelivery? {
         switch self {
         case .active(let at):
             return TPumpStatusDatum.BasalDelivery(state: .scheduled, time: at)
@@ -314,6 +314,8 @@ fileprivate extension PumpManagerStatus.BasalDeliveryState {
             return TPumpStatusDatum.BasalDelivery(state: .suspended, time: at)
         case .resuming:
             return TPumpStatusDatum.BasalDelivery(state: .resuming)
+        case .pumpInoperable:
+            return nil
         }
     }
 }

From a723ade8980438bae8193d005a91844e58d59f1f Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Mon, 10 Jun 2024 10:15:55 -0500
Subject: [PATCH 09/21] LOOP-1169 - Upload device logs (#100)

* Update for remote data service protocol changes

* Upload device logs
---
 TidepoolService.xcodeproj/project.pbxproj  |   4 +
 TidepoolServiceKit/DeviceLogUploader.swift | 130 +++++++++++++++++++
 TidepoolServiceKit/TidepoolService.swift   | 139 +++++++--------------
 3 files changed, 179 insertions(+), 94 deletions(-)
 create mode 100644 TidepoolServiceKit/DeviceLogUploader.swift

diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj
index 82cf48e..a9a0191 100644
--- a/TidepoolService.xcodeproj/project.pbxproj
+++ b/TidepoolService.xcodeproj/project.pbxproj
@@ -69,6 +69,7 @@
 		C12E4BBB288F2215009C98A2 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		C12E4BBE288F2215009C98A2 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; platformFilter = ios; };
 		C12E4BBF288F2215009C98A2 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A685422C067E410071C171 /* DeviceLogUploader.swift */; };
 		C1C9414629F0CB21008D3E05 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9414529F0CB21008D3E05 /* UIImage.swift */; };
 		C1D0B62929848A460098D215 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62829848A460098D215 /* SettingsView.swift */; };
 		C1D0B62C29848BEB0098D215 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62B29848BEB0098D215 /* Image.swift */; };
@@ -231,6 +232,7 @@
 		C199E4DA29C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1A3529629C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1A3529729C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
+		C1A685422C067E410071C171 /* DeviceLogUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLogUploader.swift; sourceTree = "<group>"; };
 		C1B0CFE129C786BF0045B04D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1B267AA2995824000BCB7C1 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1C9414529F0CB21008D3E05 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
@@ -402,6 +404,7 @@
 				A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */,
 				A913B37B24200C86000805C4 /* Extensions */,
 				A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */,
+				C1A685422C067E410071C171 /* DeviceLogUploader.swift */,
 			);
 			path = TidepoolServiceKit;
 			sourceTree = "<group>";
@@ -752,6 +755,7 @@
 				A9F9F317271A046E00D19374 /* StoredCarbEntry.swift in Sources */,
 				A9D1AC9D27B1E3C6008C5A12 /* DoseEntry.swift in Sources */,
 				A9752A9B270B941C00E50750 /* SingleQuantitySchedule.swift in Sources */,
+				C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */,
 				A9752A93270B766A00E50750 /* StoredDosingDecision.swift in Sources */,
 				A9752A97270B91E000E50750 /* Double.swift in Sources */,
 				C110888F2A39149100BA4898 /* BuildDetails.swift in Sources */,
diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift
new file mode 100644
index 0000000..61b5a18
--- /dev/null
+++ b/TidepoolServiceKit/DeviceLogUploader.swift
@@ -0,0 +1,130 @@
+//
+//  DeviceLogUploader.swift
+//  TidepoolServiceKit
+//
+//  Created by Pete Schwamb on 5/28/24.
+//  Copyright © 2024 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import os.log
+import LoopKit
+import TidepoolKit
+
+/// Periodically uploads device logs in hourly chunks to backend
+actor DeviceLogUploader {
+    private let log = OSLog(category: "DeviceLogUploader")
+
+    private let api: TAPI
+
+    private var delegate: RemoteDataServiceDelegate?
+
+    private var logChunkDuration = TimeInterval(hours: 1)
+
+    func setDelegate(_ delegate: RemoteDataServiceDelegate?) {
+        self.delegate = delegate
+    }
+
+    init(api: TAPI) {
+        self.api = api
+
+        Task {
+            await main()
+        }
+    }
+
+    func main() async {
+        let backfillLimitInterval = TimeInterval(days: 2)
+        // Default start uploading logs from 2 days ago
+        var nextUploadStart = Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration)
+
+        // Fetch device log metadata records
+        while true {
+            do {
+                // TODO: fetching logs is not implemented on the backend yet: awaiting https://tidepool.atlassian.net/browse/BACK-3011
+                // For now, we expect this to error, so the catch has been modified to break out of the loop. Once this is implemented,
+                // We will want to retry on error, so the break should eventually be removed.
+
+                var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date())
+                uploadMetadata.sort { a, b in
+                    return a.endAtTime > b.endAtTime
+                }
+                if let lastEnd = uploadMetadata.last?.endAtTime {
+                    nextUploadStart = lastEnd.dateFlooredToTimeInterval(logChunkDuration)
+                }
+                break
+            } catch {
+                log.error("Unable to fetch device log metadata: %@", String(describing: error))
+                try? await Task.sleep(nanoseconds: TimeInterval(minutes: 1).nanoseconds)
+                break // TODO: Remove when backend has implemented device log metadata fetching (see above)
+            }
+        }
+        // Start upload loop
+        while true {
+            let nextUploadEnd = nextUploadStart.addingTimeInterval(logChunkDuration)
+            let timeUntilNextUpload = nextUploadEnd.timeIntervalSinceNow
+            if timeUntilNextUpload > 0 {
+                log.debug("Waiting %@s until next upload", String(timeUntilNextUpload))
+                try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds)
+            }
+            await upload(from: nextUploadStart, to: nextUploadEnd)
+            nextUploadStart = nextUploadEnd
+        }
+    }
+
+    func upload(from start: Date, to end: Date) async {
+        log.default("Uploading from %@ to %@", String(describing: start), String(describing: end))
+        do {
+            if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) {
+                log.default("Fetched %d logs", logs.count)
+                if logs.count > 0 {
+                    let data = logs.map({
+                        entry in
+                        TDeviceLogEntry(
+                            type: entry.type.tidepoolType,
+                            managerIdentifier: entry.managerIdentifier,
+                            deviceIdentifier: entry.deviceIdentifier ?? "unknown",
+                            timestamp: entry.timestamp,
+                            message: entry.message
+                        )
+                    })
+                    do {
+                        let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
+                        log.default("metadata: %@", String(describing: metatdata))
+                        print("hi")
+                    } catch {
+                        log.error("error uploading device logs:: %@", String(describing: error))
+                        print("hi")
+                    }
+                }
+            }
+        } catch {
+            log.error("Upload failed: %@", String(describing: error))
+        }
+    }
+}
+
+extension TimeInterval {
+    var nanoseconds: UInt64 {
+        return UInt64(self * 1e+9)
+    }
+}
+
+extension DeviceLogEntryType {
+    var tidepoolType: TDeviceLogEntry.TDeviceLogEntryType {
+        switch self {
+        case .send:
+            return .send
+        case .receive:
+            return .receive
+        case .error:
+            return .error
+        case .delegate:
+            return .delegate
+        case .delegateResponse:
+            return .delegateResponse
+        case .connection:
+            return .connection
+        }
+    }
+}
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index ba7ea50..58d42e5 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -48,6 +48,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     
     public weak var stateDelegate: StatefulPluggableDelegate?
 
+    public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate?
+
     public lazy var sessionStorage: SessionStorage = KeychainManager()
 
     public let tapi: TAPI = TAPI(clientId: BuildDetails.default.tidepoolServiceClientId, redirectURL: BuildDetails.default.tidepoolServiceRedirectURL)
@@ -69,17 +71,25 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     private let log = OSLog(category: "TidepoolService")
     private let tidepoolKitLog = OSLog(category: "TidepoolKit")
 
+    private var deviceLogUploader: DeviceLogUploader?
+
     public init(hostIdentifier: String, hostVersion: String) {
         self.id = UUID().uuidString
         self.hostIdentifier = hostIdentifier
         self.hostVersion = hostVersion
 
         Task {
-            await tapi.setLogging(self)
-            await tapi.addObserver(self)
+           await finishSetup()
         }
     }
 
+    public func finishSetup() async {
+        await tapi.setLogging(self)
+        await tapi.addObserver(self)
+        deviceLogUploader = DeviceLogUploader(api: tapi)
+        await deviceLogUploader?.setDelegate(remoteDataServiceDelegate)
+    }
+
     public init?(rawState: RawStateValue) {
         self.isOnboarded = true // Assume when restoring from state, that we're onboarded
         guard let id = rawState["id"] as? String else {
@@ -96,8 +106,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
             self.session = try sessionStorage.getSession(for: sessionService)
             Task {
                 await tapi.setSession(session)
-                await tapi.setLogging(self)
-                await tapi.addObserver(self)
+                await finishSetup()
             }
         } catch let error {
             tidepoolKitLog.error("Error initializing TidepoolService %{public}@", error.localizedDescription)
@@ -275,7 +284,7 @@ extension TidepoolService: TLogging {
 
 extension TidepoolService: RemoteDataService {
 
-    public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride], completion: @escaping (Result<Bool, Error>) -> Void) {
+    public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride]) async throws {
         // TODO: https://tidepool.atlassian.net/browse/LOOP-4769
 
         // The following code is taken from previous upload code when override events where stored in settings
@@ -301,82 +310,49 @@ extension TidepoolService: RemoteDataService {
         //                               timeZoneOffset: datumTimeZoneOffset,
         //                               payload: datumPayload,
         //                               origin: origin)
-
-        completion(.success(true))
     }
 
     public var alertDataLimit: Int? { return 1000 }
 
-    public func uploadAlertData(_ stored: [SyncAlertObject], completion: @escaping (_ result: Result<Bool, Error>) -> Void) {
+    public func uploadAlertData(_ stored: [SyncAlertObject]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
-        }
-        Task {
-            do {
-                let result = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-                completion(.success(result))
-            } catch {
-                completion(.failure(error))
-            }
+            throw TidepoolServiceError.configuration
         }
+
+        let _ = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
     }
 
     public var carbDataLimit: Int? { return 1000 }
 
-    public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject], completion: @escaping (Result<Bool, Error>) -> Void) {
+    public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
+            throw TidepoolServiceError.configuration
         }
 
-        Task {
-            do {
-                let createdUploaded = try await createData(created.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-                let updatedUploaded = try await updateData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-                let deletedUploaded = try await deleteData(withSelectors: deleted.compactMap { $0.selector })
-                completion(.success(createdUploaded || updatedUploaded || deletedUploaded))
-            } catch {
-                completion(.failure(error))
-            }
-        }
+        let _ = try await createData(created.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
+        let _ = try await updateData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
+        let _ = try await deleteData(withSelectors: deleted.compactMap { $0.selector })
     }
 
     public var doseDataLimit: Int? { return 1000 }
 
-    public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry], completion: @escaping (_ result: Result<Bool, Error>) -> Void) {
+    public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
+            throw TidepoolServiceError.configuration
         }
 
-        Task {
-            do {
-                let createdUploaded = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-                let deletedUploaded = try await deleteData(withSelectors: deleted.flatMap { $0.selectors })
-                completion(.success(createdUploaded || deletedUploaded))
-            } catch {
-                completion(.failure(error))
-            }
-        }
+        let _ = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
+        let _ = try await deleteData(withSelectors: deleted.flatMap { $0.selectors })
     }
 
     public var dosingDecisionDataLimit: Int? { return 50 }  // Each can be up to 20K bytes of serialized JSON, target ~1M or less
 
-    public func uploadDosingDecisionData(_ stored: [StoredDosingDecision], completion: @escaping (_ result: Result<Bool, Error>) -> Void) {
+    public func uploadDosingDecisionData(_ stored: [StoredDosingDecision]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
+            throw TidepoolServiceError.configuration
         }
 
-        Task {
-            do {
-                let result = try await createData(calculateDosingDecisionData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion))
-                completion(.success(result))
-            } catch {
-                completion(.failure(error))
-            }
-        }
+        let _ = try await createData(calculateDosingDecisionData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion))
     }
 
     func calculateDosingDecisionData(_ stored: [StoredDosingDecision], for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] {
@@ -427,63 +403,39 @@ extension TidepoolService: RemoteDataService {
 
     public var glucoseDataLimit: Int? { return 1000 }
 
-    public func uploadGlucoseData(_ stored: [StoredGlucoseSample], completion: @escaping (Result<Bool, Error>) -> Void) {
+    public func uploadGlucoseData(_ stored: [StoredGlucoseSample]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
+            throw TidepoolServiceError.configuration
         }
 
-        Task {
-            do {
-                let result = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-                completion(.success(result))
-            } catch {
-                completion(.failure(error))
-            }
-        }
+        let _ = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
     }
 
     public var pumpDataEventLimit: Int? { return 1000 }
 
-    public func uploadPumpEventData(_ stored: [PersistedPumpEvent], completion: @escaping (_ result: Result<Bool, Error>) -> Void) {
+    public func uploadPumpEventData(_ stored: [PersistedPumpEvent]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
+            throw TidepoolServiceError.configuration
         }
 
-        Task {
-            do {
-                let result = try await createData(stored.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-                completion(.success(result))
-            } catch {
-                completion(.failure(error))
-            }
-        }
+        let _ = try await createData(stored.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
     }
 
     public var settingsDataLimit: Int? { return 400 }  // Each can be up to 2.5K bytes of serialized JSON, target ~1M or less
 
-    public func uploadSettingsData(_ stored: [StoredSettings], completion: @escaping (_ result: Result<Bool, Error>) -> Void) {
+    public func uploadSettingsData(_ stored: [StoredSettings]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            completion(.failure(TidepoolServiceError.configuration))
-            return
+            throw TidepoolServiceError.configuration
         }
 
         let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum) = calculateSettingsData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
 
-        Task {
-            do {
-                let createdUploaded = try await createData(created)
-                let updatedUploaded = try await updateData(updated)
-                self.lastControllerSettingsDatum = lastControllerSettingsDatum
-                self.lastCGMSettingsDatum = lastCGMSettingsDatum
-                self.lastPumpSettingsDatum = lastPumpSettingsDatum
-                self.completeUpdate()
-                completion(.success(createdUploaded || updatedUploaded))
-            } catch {
-                completion(.failure(error))
-            }
-        }
+        let _ = try await createData(created)
+        let _ = try await updateData(updated)
+        self.lastControllerSettingsDatum = lastControllerSettingsDatum
+        self.lastCGMSettingsDatum = lastCGMSettingsDatum
+        self.lastPumpSettingsDatum = lastPumpSettingsDatum
+        self.completeUpdate()
     }
 
     func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?) {
@@ -606,9 +558,8 @@ extension TidepoolService: RemoteDataService {
         }
     }
 
-    public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent], completion: @escaping (Result<Bool, Error>) -> Void) {
+    public func uploadCgmEventData(_ stored: [PersistedCgmEvent]) async throws {
         // TODO: Upload sensor/transmitter changes
-        completion(.success(false))
     }
 
     public func remoteNotificationWasReceived(_ notification: [String: AnyObject]) async throws {

From 0b9dbdb331cc69adb9042487435d6e8a04ba08f7 Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Tue, 27 Aug 2024 11:51:15 -0500
Subject: [PATCH 10/21] Upload temporary presets (#103)

---
 TidepoolService.xcodeproj/project.pbxproj     |   4 +
 .../Extensions/StoredSettings.swift           |  74 -----------
 .../TemporaryScheduleOverride.swift           | 120 ++++++++++++++++++
 TidepoolServiceKit/TidepoolService.swift      |  33 +----
 4 files changed, 131 insertions(+), 100 deletions(-)
 create mode 100644 TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift

diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj
index a9a0191..76a25dd 100644
--- a/TidepoolService.xcodeproj/project.pbxproj
+++ b/TidepoolService.xcodeproj/project.pbxproj
@@ -69,6 +69,7 @@
 		C12E4BBB288F2215009C98A2 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		C12E4BBE288F2215009C98A2 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; platformFilter = ios; };
 		C12E4BBF288F2215009C98A2 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		C14559A62C7CF49100541EF1 /* TemporaryScheduleOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */; };
 		C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A685422C067E410071C171 /* DeviceLogUploader.swift */; };
 		C1C9414629F0CB21008D3E05 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9414529F0CB21008D3E05 /* UIImage.swift */; };
 		C1D0B62929848A460098D215 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62829848A460098D215 /* SettingsView.swift */; };
@@ -226,6 +227,7 @@
 		C110888E2A39149100BA4898 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		C12522E1298309B5006EA1CD /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1317D4129830A0800625B94 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
+		C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryScheduleOverride.swift; sourceTree = "<group>"; };
 		C18B726B299581C600F138D3 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C192C60B29C78711001EFEA6 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C199E4D929C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -322,6 +324,7 @@
 				A97A60FA243818C900AD69A5 /* TDatum.swift */,
 				A9752A9C270B972D00E50750 /* TimeInterval.swift */,
 				A9D10DB727AB2CCF00814B7B /* SyncAlertObject.swift */,
+				C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */,
 			);
 			path = Extensions;
 			sourceTree = "<group>";
@@ -766,6 +769,7 @@
 				A98737CD2788E61400A6A23D /* InsulinType.swift in Sources */,
 				A9D1AC9B27B1E046008C5A12 /* Data.swift in Sources */,
 				A9F9F319271A05B100D19374 /* IdentifiableHKDatum.swift in Sources */,
+				C14559A62C7CF49100541EF1 /* TemporaryScheduleOverride.swift in Sources */,
 				A9057687271F770F0030C3B1 /* IdentifiableDatum.swift in Sources */,
 				A97651752421AA10002EB5D4 /* OSLog.swift in Sources */,
 				A9DAAD3622E7CAC100E76C9F /* TidepoolService.swift in Sources */,
diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift
index 9e212cb..4b9a84f 100644
--- a/TidepoolServiceKit/Extensions/StoredSettings.swift
+++ b/TidepoolServiceKit/Extensions/StoredSettings.swift
@@ -390,80 +390,6 @@ fileprivate extension TemporaryScheduleOverridePreset {
     var datumDuration: TimeInterval? { duration.isFinite ? duration.timeInterval : nil }
 }
 
-fileprivate extension TemporaryScheduleOverride {
-    var datumTime: Date { startDate }
-
-    var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType { context.datumOverrideType }
-    
-    var datumOverridePreset: String? {
-        guard case .preset(let preset) = context else {
-            return nil
-        }
-        return preset.name
-    }
-    
-    var datumMethod: TPumpSettingsOverrideDeviceEventDatum.Method? { .manual }
-    
-    var datumDuration: TimeInterval? {
-        switch duration {
-        case .finite(let interval):
-            return interval
-        case .indefinite:
-            return nil
-        }
-    }
-    
-    var datumExpectedDuration: TimeInterval? { nil }
-    
-    var datumBloodGlucoseTarget: TPumpSettingsOverrideDeviceEventDatum.BloodGlucoseTarget? { settings.datumBloodGlucoseTarget }
-    
-    var datumBasalRateScaleFactor: Double? { settings.datumBasalRateScaleFactor }
-    
-    var datumCarbohydrateRatioScaleFactor: Double? { settings.datumCarbohydrateRatioScaleFactor }
-    
-    var datumInsulinSensitivityScaleFactor: Double? { settings.datumInsulinSensitivityScaleFactor }
-
-    var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? { settings.datumUnits }
-}
-
-fileprivate extension TemporaryScheduleOverride.Context {
-    var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType {
-        switch self {
-        case .preMeal:
-            return .preprandial
-        case .legacyWorkout:
-            return .physicalActivity
-        case .preset(_):
-            return .preset
-        case .custom:
-            return .custom
-        }
-    }
-}
-
-fileprivate extension TemporaryScheduleOverrideSettings {
-    var datumBloodGlucoseTarget: TPumpSettingsDatum.BloodGlucoseTarget? {
-        guard let targetRange = targetRange else {
-            return nil
-        }
-        return TPumpSettingsDatum.BloodGlucoseTarget(low: targetRange.lowerBound.doubleValue(for: .milligramsPerDeciliter),
-                                                     high: targetRange.upperBound.doubleValue(for: .milligramsPerDeciliter))
-    }
-    
-    var datumBasalRateScaleFactor: Double? { basalRateMultiplier }
-    
-    var datumCarbohydrateRatioScaleFactor: Double? { carbRatioMultiplier }
-    
-    var datumInsulinSensitivityScaleFactor: Double? { insulinSensitivityMultiplier }
-
-    var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? {
-        guard targetRange != nil else {
-            return nil
-        }
-        return TPumpSettingsOverrideDeviceEventDatum.Units(bloodGlucose: .milligramsPerDeciliter)
-    }
-}
-
 extension TCGMSettingsDatum: TypedDatum {
     static var resolvedType: String { TDatum.DatumType.cgmSettings.rawValue }
 }
diff --git a/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift b/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift
new file mode 100644
index 0000000..2bcc203
--- /dev/null
+++ b/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift
@@ -0,0 +1,120 @@
+//
+//  TemporaryScheduleOverride.swift
+//  TidepoolServiceKit
+//
+//  Created by Pete Schwamb on 8/26/24.
+//  Copyright © 2024 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+import TidepoolKit
+import LoopKit
+
+fileprivate extension TemporaryScheduleOverride.Context {
+    var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType {
+        switch self {
+        case .preMeal:
+            return .preprandial
+        case .legacyWorkout:
+            return .physicalActivity
+        case .preset(_):
+            return .preset
+        case .custom:
+            return .custom
+        }
+    }
+}
+
+extension TemporaryScheduleOverrideSettings {
+    var datumBloodGlucoseTarget: TPumpSettingsDatum.BloodGlucoseTarget? {
+        guard let targetRange = targetRange else {
+            return nil
+        }
+        return TPumpSettingsDatum.BloodGlucoseTarget(low: targetRange.lowerBound.doubleValue(for: .milligramsPerDeciliter),
+                                                     high: targetRange.upperBound.doubleValue(for: .milligramsPerDeciliter))
+    }
+
+    var datumBasalRateScaleFactor: Double? { basalRateMultiplier }
+
+    var datumCarbohydrateRatioScaleFactor: Double? { carbRatioMultiplier }
+
+    var datumInsulinSensitivityScaleFactor: Double? { insulinSensitivityMultiplier }
+
+    var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? {
+        guard targetRange != nil else {
+            return nil
+        }
+        return TPumpSettingsOverrideDeviceEventDatum.Units(bloodGlucose: .milligramsPerDeciliter)
+    }
+}
+
+extension TemporaryScheduleOverride: IdentifiableDatum {
+
+    var syncIdentifierAsString: String { syncIdentifier.uuidString }
+
+    func datum(for userId: String, hostIdentifier: String, hostVersion: String) -> TDatum {
+        let datum = TPumpSettingsOverrideDeviceEventDatum(time: datumTime,
+                                                          overrideType: datumOverrideType,
+                                                          overridePreset: datumOverridePreset,
+                                                          method: datumMethod,
+                                                          duration: datumDuration,
+                                                          expectedDuration: datumExpectedDuration,
+                                                          bloodGlucoseTarget: datumBloodGlucoseTarget,
+                                                          basalRateScaleFactor: datumBasalRateScaleFactor,
+                                                          carbohydrateRatioScaleFactor: datumCarbohydrateRatioScaleFactor,
+                                                          insulinSensitivityScaleFactor: datumInsulinSensitivityScaleFactor,
+                                                          units: datumUnits)
+        let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+        return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self),
+                               payload: datumPayload,
+                               origin: origin)
+    }
+
+    private var datumPayload: TDictionary {
+        var dictionary = TDictionary()
+        dictionary["syncIdentifier"] = syncIdentifierAsString
+        return dictionary
+    }
+
+    var datumTime: Date { startDate }
+
+    var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType { context.datumOverrideType }
+
+    var datumOverridePreset: String? {
+        guard case .preset(let preset) = context else {
+            return nil
+        }
+        return preset.name
+    }
+
+    var datumMethod: TPumpSettingsOverrideDeviceEventDatum.Method? { .manual }
+
+    var datumDuration: TimeInterval? {
+        switch duration {
+        case .finite(let interval):
+            return interval
+        case .indefinite:
+            return nil
+        }
+    }
+
+    var datumExpectedDuration: TimeInterval? { nil }
+
+    var datumBloodGlucoseTarget: TPumpSettingsOverrideDeviceEventDatum.BloodGlucoseTarget? { settings.datumBloodGlucoseTarget }
+
+    var datumBasalRateScaleFactor: Double? { settings.datumBasalRateScaleFactor }
+
+    var datumCarbohydrateRatioScaleFactor: Double? { settings.datumCarbohydrateRatioScaleFactor }
+
+    var datumInsulinSensitivityScaleFactor: Double? { settings.datumInsulinSensitivityScaleFactor }
+
+    var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? { settings.datumUnits }
+
+}
+
+extension TemporaryScheduleOverride {
+    var selectors: [TDatum.Selector] {
+        return [datumSelector(for: TPumpSettingsOverrideDeviceEventDatum.self)]
+    }
+}
+
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 58d42e5..b1a1b1f 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -285,31 +285,12 @@ extension TidepoolService: TLogging {
 extension TidepoolService: RemoteDataService {
 
     public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride]) async throws {
-        // TODO: https://tidepool.atlassian.net/browse/LOOP-4769
-
-        // The following code is taken from previous upload code when override events where stored in settings
-        // To be implemented with
-
-        //        guard let activeOverride = activeOverride else {
-        //            return nil
-        //        }
-        //        let datum = TPumpSettingsOverrideDeviceEventDatum(time: activeOverride.datumTime,
-        //                                                          overrideType: activeOverride.datumOverrideType,
-        //                                                          overridePreset: activeOverride.datumOverridePreset,
-        //                                                          method: activeOverride.datumMethod,
-        //                                                          duration: activeOverride.datumDuration,
-        //                                                          expectedDuration: activeOverride.datumExpectedDuration,
-        //                                                          bloodGlucoseTarget: activeOverride.datumBloodGlucoseTarget,
-        //                                                          basalRateScaleFactor: activeOverride.datumBasalRateScaleFactor,
-        //                                                          carbohydrateRatioScaleFactor: activeOverride.datumCarbohydrateRatioScaleFactor,
-        //                                                          insulinSensitivityScaleFactor: activeOverride.datumInsulinSensitivityScaleFactor,
-        //                                                          units: activeOverride.datumUnits)
-        //        let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion)
-        //        return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self),
-        //                               timeZone: datumTimeZone,
-        //                               timeZoneOffset: datumTimeZoneOffset,
-        //                               payload: datumPayload,
-        //                               origin: origin)
+        guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
+            throw TidepoolServiceError.configuration
+        }
+
+        let _ = try await createData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
+        let _ = try await deleteData(withSelectors: deleted.flatMap { $0.selectors })
     }
 
     public var alertDataLimit: Int? { return 1000 }
@@ -440,7 +421,7 @@ extension TidepoolService: RemoteDataService {
 
     func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?) {
         var created: [TDatum] = []
-        var updated: [TDatum] = []
+        let updated: [TDatum] = []
         var lastControllerSettingsDatum = lastControllerSettingsDatum
         var lastCGMSettingsDatum = lastCGMSettingsDatum
         var lastPumpSettingsDatum = lastPumpSettingsDatum

From 5caeee373a8cddac4324145d4cc3dd13b9edafcd Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Fri, 13 Sep 2024 16:10:37 -0500
Subject: [PATCH 11/21] LOOP-4098 - Specify correct duration for basal segments
 (#104)

* Specify correct duration for basal segments

* Fix test
---
 TidepoolServiceKit/Extensions/DoseEntry.swift           | 4 ++--
 TidepoolServiceKitTests/Extensions/DoseEntryTests.swift | 1 -
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift
index c9f7c85..73b1461 100644
--- a/TidepoolServiceKit/Extensions/DoseEntry.swift
+++ b/TidepoolServiceKit/Extensions/DoseEntry.swift
@@ -82,8 +82,8 @@ extension DoseEntry: IdentifiableDatum {
         payload["deliveredUnits"] = datumBasalDeliveredUnits
 
         var datum = TAutomatedBasalDatum(time: datumTime,
-                                         duration: !isMutable ? datumDuration : 0,
-                                         expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
+                                         duration: datumDuration,
+                                         expectedDuration: nil,
                                          rate: datumScheduledBasalRate,
                                          scheduleName: StoredSettings.activeScheduleNameDefault,
                                          insulinFormulation: datumInsulinFormulation)
diff --git a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift
index 56d6a9c..5027623 100644
--- a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift
+++ b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift
@@ -38,7 +38,6 @@ class DoseEntryDataTests: XCTestCase {
   {
     "deliveryType" : "automated",
     "duration" : 1500000,
-    "expectedDuration" : 1800000,
     "id" : "f839af02f6832d7c81d636dbbbadbc01",
     "insulinFormulation" : {
       "simple" : {

From e192e327530e598c8acffabf14af970001b5a333 Mon Sep 17 00:00:00 2001
From: Nathaniel Hamming <nate@hmtconsulting.org>
Date: Wed, 18 Sep 2024 16:21:18 -0300
Subject: [PATCH 12/21] [LOOP-5035] report time zone changes (#105)

* report time zone changes

* removed unneeded print statement

* set device log uploader when remoteDataServiceDelegate is set
---
 TidepoolServiceKit/DeviceLogUploader.swift |  2 -
 TidepoolServiceKit/TidepoolService.swift   | 44 ++++++++++++++++++++--
 2 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift
index 61b5a18..4ed7344 100644
--- a/TidepoolServiceKit/DeviceLogUploader.swift
+++ b/TidepoolServiceKit/DeviceLogUploader.swift
@@ -91,10 +91,8 @@ actor DeviceLogUploader {
                     do {
                         let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
                         log.default("metadata: %@", String(describing: metatdata))
-                        print("hi")
                     } catch {
                         log.error("error uploading device logs:: %@", String(describing: error))
-                        print("hi")
                     }
                 }
             }
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index b1a1b1f..3089277 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -48,8 +48,14 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     
     public weak var stateDelegate: StatefulPluggableDelegate?
 
-    public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate?
-
+    public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate? {
+        didSet {
+            Task {
+                await setDeviceLogUploaderDelegate()
+            }
+        }
+    }
+    
     public lazy var sessionStorage: SessionStorage = KeychainManager()
 
     public let tapi: TAPI = TAPI(clientId: BuildDetails.default.tidepoolServiceClientId, redirectURL: BuildDetails.default.tidepoolServiceRedirectURL)
@@ -72,6 +78,10 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     private let tidepoolKitLog = OSLog(category: "TidepoolKit")
 
     private var deviceLogUploader: DeviceLogUploader?
+    
+    private func setDeviceLogUploaderDelegate() async {
+        await deviceLogUploader?.setDelegate(remoteDataServiceDelegate)
+    }
 
     public init(hostIdentifier: String, hostVersion: String) {
         self.id = UUID().uuidString
@@ -87,7 +97,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
         await tapi.setLogging(self)
         await tapi.addObserver(self)
         deviceLogUploader = DeviceLogUploader(api: tapi)
-        await deviceLogUploader?.setDelegate(remoteDataServiceDelegate)
+        await setDeviceLogUploaderDelegate()
+        observeTimeZoneChanges()
     }
 
     public init?(rawState: RawStateValue) {
@@ -485,6 +496,33 @@ extension TidepoolService: RemoteDataService {
 
         return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum)
     }
+    
+    private func uploadTimeZoneChangeData(from fromTimeZone: TimeZone, to toTimeZone: TimeZone, method: TTimeChangeDeviceEventDatum.Method = .automatic, at date: Date = Date()) async throws {
+        guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
+            throw TidepoolServiceError.configuration
+        }
+
+        let timeZoneChangeData = TTimeChangeDeviceEventDatum(time: date,
+                                                             from: TTimeChangeDeviceEventDatum.Info(timeZoneName: fromTimeZone.identifier),
+                                                             to: TTimeChangeDeviceEventDatum.Info(timeZoneName: toTimeZone.identifier),
+                                                             method: method)
+        let _ = try await createData([timeZoneChangeData])
+    }
+    
+    private func observeTimeZoneChanges() {
+        NotificationCenter.default.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: .main) { notification in
+            if let previousTimeZone = notification.object as? TimeZone {
+                let currentTimeZone = TimeZone.current
+                Task {
+                    do {
+                        try await self.uploadTimeZoneChangeData(from: previousTimeZone, to: currentTimeZone)
+                    } catch {
+                        self.log.error("Failed to upload time zone change data - %{public}@", error.localizedDescription)
+                    }
+                }
+            }
+        }
+    }
 
     private func createData(_ data: [TDatum]) async throws -> Bool {
         if let error = error {

From b98c3113fda650d04537cef2ecca566fa6d61d88 Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Wed, 18 Sep 2024 14:30:14 -0500
Subject: [PATCH 13/21] LOOP-5065 Device log upload fixes (#106)

* Device log upload fixes

* Fix comment to match code
---
 TidepoolServiceKit/DeviceLogUploader.swift | 113 +++++++++++----------
 1 file changed, 59 insertions(+), 54 deletions(-)

diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift
index 4ed7344..b27b524 100644
--- a/TidepoolServiceKit/DeviceLogUploader.swift
+++ b/TidepoolServiceKit/DeviceLogUploader.swift
@@ -21,6 +21,9 @@ actor DeviceLogUploader {
 
     private var logChunkDuration = TimeInterval(hours: 1)
 
+    private let backfillLimitInterval = TimeInterval(days: 2)
+
+
     func setDelegate(_ delegate: RemoteDataServiceDelegate?) {
         self.delegate = delegate
     }
@@ -34,70 +37,72 @@ actor DeviceLogUploader {
     }
 
     func main() async {
-        let backfillLimitInterval = TimeInterval(days: 2)
-        // Default start uploading logs from 2 days ago
-        var nextUploadStart = Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration)
+        var nextLogStart: Date?
 
-        // Fetch device log metadata records
+        // Start upload loop
         while true {
-            do {
-                // TODO: fetching logs is not implemented on the backend yet: awaiting https://tidepool.atlassian.net/browse/BACK-3011
-                // For now, we expect this to error, so the catch has been modified to break out of the loop. Once this is implemented,
-                // We will want to retry on error, so the break should eventually be removed.
-
-                var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date())
-                uploadMetadata.sort { a, b in
-                    return a.endAtTime > b.endAtTime
+            if nextLogStart == nil {
+                do {
+                    nextLogStart = try await getMostRecentUploadEndTime()
+                } catch {
+                    log.error("Unable to fetch device log metadata: %{public}@", String(describing: error))
                 }
-                if let lastEnd = uploadMetadata.last?.endAtTime {
-                    nextUploadStart = lastEnd.dateFlooredToTimeInterval(logChunkDuration)
+            }
+
+            if nextLogStart != nil {
+                let nextLogEnd = nextLogStart!.addingTimeInterval(logChunkDuration)
+                let timeUntilNextUpload = nextLogEnd.timeIntervalSinceNow
+                if timeUntilNextUpload > 0 {
+                    log.debug("Waiting %{public}@s until next upload", String(timeUntilNextUpload))
+                    try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds)
+                }
+                do {
+                    try await upload(from: nextLogStart!, to: nextLogEnd)
+                    nextLogStart = nextLogEnd
+                } catch {
+                    log.error("Upload failed: %{public}@", String(describing: error))
+                    // Upload failed, retry in 5 minutes.
+                    try? await Task.sleep(nanoseconds: TimeInterval(minutes: 5).nanoseconds)
                 }
-                break
-            } catch {
-                log.error("Unable to fetch device log metadata: %@", String(describing: error))
-                try? await Task.sleep(nanoseconds: TimeInterval(minutes: 1).nanoseconds)
-                break // TODO: Remove when backend has implemented device log metadata fetching (see above)
+            } else {
+                // Haven't been able to talk to backend to find any previous log uploads. Retry in 15 minutes.
+                try? await Task.sleep(nanoseconds: TimeInterval(minutes: 15).nanoseconds)
             }
         }
-        // Start upload loop
-        while true {
-            let nextUploadEnd = nextUploadStart.addingTimeInterval(logChunkDuration)
-            let timeUntilNextUpload = nextUploadEnd.timeIntervalSinceNow
-            if timeUntilNextUpload > 0 {
-                log.debug("Waiting %@s until next upload", String(timeUntilNextUpload))
-                try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds)
-            }
-            await upload(from: nextUploadStart, to: nextUploadEnd)
-            nextUploadStart = nextUploadEnd
+    }
+
+    func getMostRecentUploadEndTime() async throws -> Date {
+        var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date())
+        uploadMetadata.sort { a, b in
+            return a.endAtTime < b.endAtTime
+        }
+        if let lastEnd = uploadMetadata.last?.endAtTime {
+            return lastEnd
+        } else {
+            // No previous uploads found in last two days
+            return Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration)
         }
     }
 
-    func upload(from start: Date, to end: Date) async {
-        log.default("Uploading from %@ to %@", String(describing: start), String(describing: end))
-        do {
-            if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) {
-                log.default("Fetched %d logs", logs.count)
-                if logs.count > 0 {
-                    let data = logs.map({
-                        entry in
-                        TDeviceLogEntry(
-                            type: entry.type.tidepoolType,
-                            managerIdentifier: entry.managerIdentifier,
-                            deviceIdentifier: entry.deviceIdentifier ?? "unknown",
-                            timestamp: entry.timestamp,
-                            message: entry.message
-                        )
-                    })
-                    do {
-                        let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
-                        log.default("metadata: %@", String(describing: metatdata))
-                    } catch {
-                        log.error("error uploading device logs:: %@", String(describing: error))
-                    }
-                }
+    func upload(from start: Date, to end: Date) async throws {
+        if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) {
+            if logs.count > 0 {
+                let data = logs.map({
+                    entry in
+                    TDeviceLogEntry(
+                        type: entry.type.tidepoolType,
+                        managerIdentifier: entry.managerIdentifier,
+                        deviceIdentifier: entry.deviceIdentifier ?? "unknown",
+                        timestamp: entry.timestamp,
+                        message: entry.message
+                    )
+                })
+                let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
+                log.debug("Uploaded %d entries from %{public}@ to %{public}@", logs.count, String(describing: start), String(describing: end))
+                log.debug("metadata: %{public}@", String(describing: metatdata))
+            } else {
+                log.debug("No device log entries from %{public}@ to %{public}@", String(describing: start), String(describing: end))
             }
-        } catch {
-            log.error("Upload failed: %@", String(describing: error))
         }
     }
 }

From 78ed7b7a6b83f734cd3d46aa55d9254ea62215f1 Mon Sep 17 00:00:00 2001
From: Nathaniel Hamming <nate@hmtconsulting.org>
Date: Mon, 23 Sep 2024 13:38:39 -0300
Subject: [PATCH 14/21] [LOOP-5035] Pump event time zone sync (#107)

* reverting code

* uploading pump time zone sync event

* response to comments
---
 .../Extensions/PersistedPumpEvent.swift       | 33 +++++++++++++++++++
 TidepoolServiceKit/TidepoolService.swift      | 31 +----------------
 2 files changed, 34 insertions(+), 30 deletions(-)

diff --git a/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift b/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift
index b926997..489f9c1 100644
--- a/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift
+++ b/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift
@@ -57,6 +57,8 @@ extension PersistedPumpEvent: IdentifiableDatum {
             return dataForRewind(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
         case .suspend:
             return dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+        case .timeZoneSync:
+            return dataForTimeZoneSync(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
         default:
             return []
         }
@@ -173,6 +175,33 @@ extension PersistedPumpEvent: IdentifiableDatum {
                                 origin: origin)
         return [datum]
     }
+    
+    private func dataForTimeZoneSync(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] {
+        guard let type = type,
+              case let .timeZoneSync(fromSecondsFromGMT, toSecondsFromGMT) = type
+        else {
+            return []
+        }
+        
+        let fromTime = formattedDateWithoutTimeZoneOffset(date, for: TimeZone(secondsFromGMT: fromSecondsFromGMT))
+        let toTime = formattedDateWithoutTimeZoneOffset(date, for: TimeZone(secondsFromGMT: toSecondsFromGMT))
+        var datum = TTimeChangeDeviceEventDatum(time: date,
+                                                from: TTimeChangeDeviceEventDatum.Info(time: fromTime),
+                                                to: TTimeChangeDeviceEventDatum.Info(time: toTime),
+                                                method: .manual)
+        let origin = datumOrigin(for: resolvedIdentifier(for: TTimeChangeDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+        datum = datum.adornWith(id: datumId(for: userId, type: TTimeChangeDeviceEventDatum.self),
+                                payload: datumPayload,
+                                origin: origin)
+        return [datum]
+    }
+    
+    private func formattedDateWithoutTimeZoneOffset(_ date: Date, for timeZone: TimeZone?) -> String {
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
+        dateFormatter.timeZone = timeZone
+        return dateFormatter.string(from: date)
+    }
 
     private var datumTime: Date { dose?.startDate ?? date }
 
@@ -223,3 +252,7 @@ extension TReservoirChangeDeviceEventDatum: TypedDatum {
 extension TStatusDeviceEventDatum: TypedDatum {
     static var resolvedType: String { "\(TDatum.DatumType.deviceEvent.rawValue)/\(TDeviceEventDatum.SubType.status.rawValue)" }
 }
+
+extension TTimeChangeDeviceEventDatum: TypedDatum {
+    static var resolvedType: String { "\(TDatum.DatumType.deviceEvent.rawValue)/\(TDeviceEventDatum.SubType.timeChange.rawValue)" }
+}
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 3089277..e8a5719 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -64,7 +64,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
 
     private let id: String
 
-
     private var lastControllerSettingsDatum: TControllerSettingsDatum?
 
     private var lastCGMSettingsDatum: TCGMSettingsDatum?
@@ -78,7 +77,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     private let tidepoolKitLog = OSLog(category: "TidepoolKit")
 
     private var deviceLogUploader: DeviceLogUploader?
-    
+
     private func setDeviceLogUploaderDelegate() async {
         await deviceLogUploader?.setDelegate(remoteDataServiceDelegate)
     }
@@ -98,7 +97,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
         await tapi.addObserver(self)
         deviceLogUploader = DeviceLogUploader(api: tapi)
         await setDeviceLogUploaderDelegate()
-        observeTimeZoneChanges()
     }
 
     public init?(rawState: RawStateValue) {
@@ -496,33 +494,6 @@ extension TidepoolService: RemoteDataService {
 
         return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum)
     }
-    
-    private func uploadTimeZoneChangeData(from fromTimeZone: TimeZone, to toTimeZone: TimeZone, method: TTimeChangeDeviceEventDatum.Method = .automatic, at date: Date = Date()) async throws {
-        guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
-            throw TidepoolServiceError.configuration
-        }
-
-        let timeZoneChangeData = TTimeChangeDeviceEventDatum(time: date,
-                                                             from: TTimeChangeDeviceEventDatum.Info(timeZoneName: fromTimeZone.identifier),
-                                                             to: TTimeChangeDeviceEventDatum.Info(timeZoneName: toTimeZone.identifier),
-                                                             method: method)
-        let _ = try await createData([timeZoneChangeData])
-    }
-    
-    private func observeTimeZoneChanges() {
-        NotificationCenter.default.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: .main) { notification in
-            if let previousTimeZone = notification.object as? TimeZone {
-                let currentTimeZone = TimeZone.current
-                Task {
-                    do {
-                        try await self.uploadTimeZoneChangeData(from: previousTimeZone, to: currentTimeZone)
-                    } catch {
-                        self.log.error("Failed to upload time zone change data - %{public}@", error.localizedDescription)
-                    }
-                }
-            }
-        }
-    }
 
     private func createData(_ data: [TDatum]) async throws -> Bool {
         if let error = error {

From 54f12b50223150178142141a0494a50b574af274 Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Wed, 2 Oct 2024 17:18:39 -0500
Subject: [PATCH 15/21] LOOP-5071 Overlay basal and automation history (#108)

* Overlay basal and automation history

* Test updates

* Annotate suspends with scheduled basal
---
 TidepoolServiceKit/Extensions/DoseEntry.swift | 193 +++++++++++++++++-
 TidepoolServiceKit/TidepoolService.swift      |  29 ++-
 .../Extensions/DoseEntryTests.swift           | 133 +++++++++++-
 3 files changed, 350 insertions(+), 5 deletions(-)

diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift
index 73b1461..257d6eb 100644
--- a/TidepoolServiceKit/Extensions/DoseEntry.swift
+++ b/TidepoolServiceKit/Extensions/DoseEntry.swift
@@ -8,6 +8,8 @@
 
 import LoopKit
 import TidepoolKit
+import LoopAlgorithm
+import HealthKit
 
 /*
  DoseEntry
@@ -204,8 +206,8 @@ extension DoseEntry: IdentifiableDatum {
         payload["deliveredUnits"] = deliveredUnits
 
         var datum = TAutomatedBasalDatum(time: datumTime,
-                                         duration: !isMutable ? datumDuration : 0,
-                                         expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
+                                         duration: datumDuration,
+                                         expectedDuration: datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil,
                                          rate: datumRate,
                                          scheduleName: StoredSettings.activeScheduleNameDefault,
                                          insulinFormulation: datumInsulinFormulation)
@@ -342,3 +344,190 @@ extension TNormalBolusDatum: TypedDatum {
 extension TInsulinDatum: TypedDatum {
     static var resolvedType: String { TDatum.DatumType.insulin.rawValue }
 }
+
+extension DoseEntry {
+
+    /// Annotates a dose with the context of a history of scheduled basal rates
+    ///
+    /// If the dose crosses a schedule boundary, it will be split into multiple doses so each dose has a
+    /// single scheduled basal rate.
+    ///
+    /// - Parameter basalHistory: The history of basal schedule values to apply. Only schedule values overlapping the dose should be included.
+    /// - Returns: An array of annotated doses
+    fileprivate func annotated(with basalHistory: [AbsoluteScheduleValue<Double>]) -> [DoseEntry] {
+
+        guard type == .tempBasal || type == .suspend, !basalHistory.isEmpty else {
+            return [self]
+        }
+
+        if type == .suspend {
+            guard value == 0 else {
+                preconditionFailure("suspend with non-zero delivery")
+            }
+        } else {
+            guard unit != .units else {
+                preconditionFailure("temp basal without rate unsupported")
+            }
+        }
+
+        if isMutable {
+            var newDose = self
+            let basal = basalHistory.first!
+            newDose.scheduledBasalRate = HKQuantity(unit: .internationalUnitsPerHour, doubleValue: basal.value)
+            return [newDose]
+        }
+
+        var doses: [DoseEntry] = []
+
+        for (index, basalItem) in basalHistory.enumerated() {
+            let startDate: Date
+            let endDate: Date
+
+            if index == 0 {
+                startDate = self.startDate
+            } else {
+                startDate = basalItem.startDate
+            }
+
+            if index == basalHistory.count - 1 {
+                endDate = self.endDate
+            } else {
+                endDate = basalHistory[index + 1].startDate
+            }
+
+            let segmentStartDate = max(startDate, self.startDate)
+            let segmentEndDate = max(startDate, min(endDate, self.endDate))
+            let segmentDuration = segmentEndDate.timeIntervalSince(segmentStartDate)
+            let segmentPortion = (segmentDuration / duration)
+
+            var annotatedDose = self
+            annotatedDose.startDate = segmentStartDate
+            annotatedDose.endDate = segmentEndDate
+            annotatedDose.scheduledBasalRate = HKQuantity(unit: .internationalUnitsPerHour, doubleValue: basalItem.value)
+
+            if let deliveredUnits {
+                annotatedDose.deliveredUnits = deliveredUnits * segmentPortion
+            }
+
+            doses.append(annotatedDose)
+        }
+
+        if doses.count > 1 {
+            for (index, dose) in doses.enumerated() {
+                if let originalIdentifier = dose.syncIdentifier, index>0 {
+                    doses[index].syncIdentifier = originalIdentifier + "\(index+1)/\(doses.count)"
+                }
+            }
+        }
+
+        return doses
+    }
+    
+}
+
+
+extension Collection where Element == DoseEntry {
+
+    /// Annotates a sequence of dose entries with the configured basal history
+    ///
+    /// Doses which cross time boundaries in the basal rate schedule are split into multiple entries.
+    ///
+    /// - Parameter basalHistory: A history of basal rates covering the timespan of these doses.
+    /// - Returns: An array of annotated dose entries
+    public func annotated(with basalHistory: [AbsoluteScheduleValue<Double>]) -> [DoseEntry] {
+        var annotatedDoses: [DoseEntry] = []
+
+        for dose in self {
+            let basalItems = basalHistory.filterDateRange(dose.startDate, dose.endDate)
+            annotatedDoses += dose.annotated(with: basalItems)
+        }
+
+        return annotatedDoses
+    }
+
+
+    /// Assigns an automation status to any dose where automation is not already specified
+    ///
+    /// - Parameters:
+    ///   - automationHistory: A history of automation periods.
+    /// - Returns: An array of doses, with the automation flag set based on automation history. Doses will be split if the automation state changes mid-dose.
+
+    public func overlayAutomationHistory(
+        _ automationHistory: [AbsoluteScheduleValue<Bool>]
+    ) -> [DoseEntry] {
+
+        guard count > 0 else {
+            return []
+        }
+
+        var newEntries = [DoseEntry]()
+
+        var automation = automationHistory
+
+        // Assume automation if doses start before automationHistory
+        if let firstAutomation = automation.first, firstAutomation.startDate > first!.startDate {
+            automation.insert(AbsoluteScheduleValue(startDate: first!.startDate, endDate: firstAutomation.startDate, value: true), at: 0)
+        }
+
+        // Overlay automation periods
+        func annotateDoseWithAutomation(dose: DoseEntry) {
+
+            var addedCount = 0
+            for period in automation {
+                if period.endDate > dose.startDate && period.startDate < dose.endDate {
+                    var newDose = dose
+
+                    if dose.isMutable {
+                        newDose.automatic = period.value
+                        newEntries.append(newDose)
+                        return
+                    }
+
+                    newDose.startDate = Swift.max(period.startDate, dose.startDate)
+                    newDose.endDate = Swift.min(period.endDate, dose.endDate)
+                    if let delivered = dose.deliveredUnits {
+                        newDose.deliveredUnits = newDose.duration / dose.duration * delivered
+                    }
+                    newDose.automatic = period.value
+                    if addedCount > 0 {
+                        newDose.syncIdentifier = "\(dose.syncIdentifierAsString)\(addedCount+1)"
+                    }
+                    newEntries.append(newDose)
+                    addedCount += 1
+                }
+            }
+            if addedCount == 0 {
+                // automation history did not cover dose; mark automatic as default
+                var newDose = dose
+                newDose.automatic = true
+                newEntries.append(newDose)
+            }
+        }
+
+        for dose in self {
+            switch dose.type {
+            case .tempBasal, .basal, .suspend:
+                if dose.automatic == nil {
+                    annotateDoseWithAutomation(dose: dose)
+                } else {
+                    newEntries.append(dose)
+                }
+            default:
+                newEntries.append(dose)
+                break
+            }
+        }
+        return newEntries
+    }
+
+}
+
+extension DoseEntry {
+    var simpleDesc: String {
+        let seconds = Int(duration)
+        let automatic = automatic?.description ?? "na"
+        return "\(startDate) (\(seconds)s) - \(type) - isMutable:\(isMutable) automatic:\(automatic) value:\(value) delivered:\(String(describing: deliveredUnits)) scheduled:\(String(describing: scheduledBasalRate)) syncId:\(String(describing: syncIdentifier))"
+    }
+}
+
+
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index e8a5719..c7fb62e 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -326,13 +326,38 @@ extension TidepoolService: RemoteDataService {
 
     public var doseDataLimit: Int? { return 1000 }
 
+    private func annotateDoses(_ doses: [DoseEntry]) async throws -> [DoseEntry] {
+        guard !doses.isEmpty else {
+            return []
+        }
+
+        guard let remoteDataServiceDelegate else {
+            throw TidepoolServiceError.configuration
+        }
+
+        let start = doses.map { $0.startDate }.min()!
+        let end = doses.map { $0.endDate }.max()!
+
+        let basal = try await remoteDataServiceDelegate.getBasalHistory(startDate: start, endDate: end)
+        let dosesWithBasal = doses.annotated(with: basal)
+
+        let automationHistory = try await remoteDataServiceDelegate.automationHistory(from: start, to: end)
+        return dosesWithBasal.overlayAutomationHistory(automationHistory)
+
+    }
+
     public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry]) async throws {
         guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else {
             throw TidepoolServiceError.configuration
         }
 
-        let _ = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
-        let _ = try await deleteData(withSelectors: deleted.flatMap { $0.selectors })
+        // Syncidentifiers may be changed
+        let annotatedCreated = try await annotateDoses(created)
+        let _ = try await createData(annotatedCreated.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })
+
+        // annotating these so we get the correct syncIdentifiers to delete
+        let annotatedDeleted = try await annotateDoses(deleted)
+        let _ = try await deleteData(withSelectors: annotatedDeleted.flatMap { $0.selectors })
     }
 
     public var dosingDecisionDataLimit: Int? { return 50 }  // Each can be up to 20K bytes of serialized JSON, target ~1M or less
diff --git a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift
index 5027623..06a2a10 100644
--- a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift
+++ b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift
@@ -13,6 +13,7 @@ import Foundation
 import HealthKit
 import LoopKit
 import TidepoolKit
+import LoopAlgorithm
 @testable import TidepoolServiceKit
 
 class DoseEntryDataTests: XCTestCase {
@@ -549,7 +550,8 @@ class DoseEntryDataTests: XCTestCase {
       }
     ],
     "deliveryType" : "automated",
-    "duration" : 0,
+    "duration" : 1200000,
+    "expectedDuration" : 1800000,
     "id" : "f839af02f6832d7c81d636dbbbadbc01",
     "insulinFormulation" : {
       "simple" : {
@@ -720,5 +722,134 @@ class DoseEntrySelectorTests: XCTestCase {
         XCTAssertEqual(doseEntry.selectors, [TDatum.Selector(origin: TDatum.Selector.Origin(id: "ab0a722d639669875017a899a5214677:basal/automated"))])
     }
 
+    func testOverlayAutomationHistory_NoAutomationHistory() {
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: Date(), endDate: Date().addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false)
+        ]
+        let result = doses.overlayAutomationHistory([])
+
+        XCTAssertEqual(result.count, 1)
+        XCTAssertEqual(result[0].automatic, true) // Default to true when no automation history
+    }
+
+    func testOverlayAutomationHistory_SingleAutomationPeriod() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(3600), value: false)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 1)
+        XCTAssertEqual(result[0].automatic, false)
+    }
+
+    func testOverlayAutomationHistory_MultipleAutomationPeriods() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(1800), value: false),
+            AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(3600), value: true)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 2)
+        XCTAssertEqual(result[0].automatic, false)
+        XCTAssertEqual(result[1].automatic, true)
+    }
+
+    func testOverlayAutomationHistory_PartialOverlap() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(4800), value: false)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 2)
+        XCTAssertEqual(result[0].automatic, true)
+        XCTAssertEqual(result[1].automatic, false)
+    }
+
+
+    func testOverlayAutomationHistory_NonBasalDoses() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .bolus, startDate: now, endDate: now.addingTimeInterval(300), value: 2.0, unit: .unitsPerHour, automatic: nil, manuallyEntered: false, isMutable: false),
+            DoseEntry(type: .basal, startDate: now.addingTimeInterval(300), endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, automatic: nil, manuallyEntered: false, isMutable: false)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(3600), value: false)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 2)
+        XCTAssertNil(result[0].automatic) // Bolus dose should remain unchanged
+        XCTAssertEqual(result[1].automatic, false)
+    }
+
+    func testOverlayAutomationHistory_PreexistingAutomationFlag() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, automatic: true, manuallyEntered: false, isMutable: false)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(3600), value: false)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 1)
+        XCTAssertEqual(result[0].automatic, true) // Should not change preexisting automation flag
+    }
+
+    func testOverlayAutomationHistory_DeliveredUnitsAdjustment() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, deliveredUnits: 1.0, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(1800), value: false),
+            AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(3600), value: true)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 2)
+        XCTAssertEqual(result[0].deliveredUnits!, 0.5, accuracy: 0.001)
+        XCTAssertEqual(result[0].automatic, false)
+        XCTAssertEqual(result[1].deliveredUnits!, 0.5, accuracy: 0.001)
+        XCTAssertEqual(result[1].automatic, true)
+    }
+
+    func testOverlayAutomationHistory_MutableDose() {
+        let now = Date()
+        let doses: [DoseEntry] = [
+            DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, deliveredUnits: 1.0, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: true)
+        ]
+        let automationHistory: [AbsoluteScheduleValue<Bool>] = [
+            AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(1800), value: false),
+            AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(3600), value: true)
+        ]
+
+        let result = doses.overlayAutomationHistory(automationHistory)
+
+        XCTAssertEqual(result.count, 1)
+        XCTAssertEqual(result[0].deliveredUnits!, 1, accuracy: 0.001)
+        XCTAssertEqual(result[0].automatic, false)
+        XCTAssertEqual(result[0].duration, TimeInterval(hours: 1))
+    }
+
+
     private static let dateFormatter = ISO8601DateFormatter()
 }

From e7ad5fe15a1757248f93bc266dd45d2fc2d2e7ef Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Fri, 25 Oct 2024 14:38:49 -0500
Subject: [PATCH 16/21] Use continue button during onboarding (#110)

---
 TidepoolServiceKitUI/SettingsView.swift       | 21 ++++++++++++++++---
 TidepoolServiceKitUI/TidepoolService+UI.swift | 18 +++++++++-------
 2 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift
index 3280516..bacffd3 100644
--- a/TidepoolServiceKitUI/SettingsView.swift
+++ b/TidepoolServiceKitUI/SettingsView.swift
@@ -25,12 +25,13 @@ public struct SettingsView: View {
 
     private let login: ((TEnvironment) async throws -> Void)?
     private let dismiss: (() -> Void)?
+    private let onboarding: Bool
 
     var isLoggedIn: Bool {
         return service.session != nil
     }
 
-    public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?)
+    public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?, onboarding: Bool)
     {
         let tapi = service.tapi
         self.service = service
@@ -38,6 +39,7 @@ public struct SettingsView: View {
         self._selectedEnvironment = State(initialValue: service.session?.environment ?? defaultEnvironment ?? TEnvironment.productionEnvironment)
         self.login = login
         self.dismiss = dismiss
+        self.onboarding = onboarding
     }
 
     public var body: some View {
@@ -95,8 +97,10 @@ public struct SettingsView: View {
                             .padding()
                         }
                         Spacer()
-                        if isLoggedIn {
+                        if isLoggedIn && !onboarding {
                             deleteServiceButton
+                        } else if isLoggedIn {
+                            continueButton
                         } else {
                             loginButton
                         }
@@ -177,6 +181,17 @@ public struct SettingsView: View {
         .disabled(isLoggingIn)
     }
 
+    private var continueButton: some View {
+        Button(action: {
+            dismiss?()
+        }) {
+            Text(LocalizedString("Continue", comment: "Delete Tidepool service button title"))
+        }
+        .buttonStyle(ActionButtonStyle(.primary))
+        .disabled(isLoggingIn)
+    }
+
+
     private func loginButtonTapped() {
         guard !isLoggingIn else {
             return
@@ -211,6 +226,6 @@ public struct SettingsView: View {
 struct SettingsView_Previews: PreviewProvider {
     @MainActor
     static var previews: some View {
-        SettingsView(service: TidepoolService(hostIdentifier: "Previews", hostVersion: "1.0"), login: nil, dismiss: nil)
+        SettingsView(service: TidepoolService(hostIdentifier: "Previews", hostVersion: "1.0"), login: nil, dismiss: nil, onboarding: false)
     }
 }
diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift
index b71f023..06e3d2c 100644
--- a/TidepoolServiceKitUI/TidepoolService+UI.swift
+++ b/TidepoolServiceKitUI/TidepoolService+UI.swift
@@ -29,12 +29,12 @@ enum TidepoolServiceError: Error {
     case missingWindow
 }
 
-extension TidepoolService: ServiceUI {
+extension TidepoolService: @retroactive ServiceUI {
     public static var image: UIImage? {
         UIImage(frameworkImage: "Tidepool Logo")
     }
 
-    public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult<ServiceViewController, ServiceUI> {
+    public static func setupViewController(pluginHost: PluginHost, onboarding: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> {
 
         let navController = ServiceNavigationController()
         navController.isNavigationBarHidden = true
@@ -48,7 +48,7 @@ extension TidepoolService: ServiceUI {
                     throw TidepoolServiceError.missingWindow
                 }
 
-                let windowContextProvider = WindowContextProvider(window: window)
+                let windowContextProvider = await WindowContextProvider(window: window)
                 let sessionProvider = await ASWebAuthenticationSessionProvider(contextProviding: windowContextProvider)
                 let auth = OAuth2Authenticator(api: service.tapi, environment: environment, sessionProvider: sessionProvider)
                 try await auth.login()
@@ -58,15 +58,19 @@ extension TidepoolService: ServiceUI {
                 Task {
                     await navController.notifyComplete()
                 }
-            })
+            }, onboarding: onboarding)
 
             let hostingController = await UIHostingController(rootView: settingsView)
             await navController.pushViewController(hostingController, animated: false)
         }
-        
+
         return .userInteractionRequired(navController)
     }
 
+    public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult<ServiceViewController, ServiceUI> {
+        return setupViewController(pluginHost: pluginHost, onboarding: false)
+    }
+
     public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController {
 
         let navController = ServiceNavigationController()
@@ -79,7 +83,7 @@ extension TidepoolService: ServiceUI {
                         throw TidepoolServiceError.missingWindow
                     }
 
-                    let windowContextProvider = WindowContextProvider(window: window)
+                    let windowContextProvider = await WindowContextProvider(window: window)
                     let sessionProvider = await ASWebAuthenticationSessionProvider(contextProviding: windowContextProvider)
                     let auth = OAuth2Authenticator(api: self.tapi, environment: environment, sessionProvider: sessionProvider)
                     try await auth.login()
@@ -88,7 +92,7 @@ extension TidepoolService: ServiceUI {
                 Task {
                     await navController.notifyComplete()
                 }
-            })
+            }, onboarding: false)
 
             let hostingController = await UIHostingController(rootView: settingsView)
             await navController.pushViewController(hostingController, animated: false)

From 65e5c778e16bf583303dddba9bf010fbae48988d Mon Sep 17 00:00:00 2001
From: Cameron Ingham <camji555@gmail.com>
Date: Tue, 29 Oct 2024 11:37:33 -0700
Subject: [PATCH 17/21] [LOOP-5132] Remove automatedDelivery and serialNumber
 from TPumpSettingsDatum and TCGMSettingsDatum

---
 TidepoolServiceKit/TidepoolService.swift | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index c7fb62e..699f06b 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -468,9 +468,12 @@ extension TidepoolService: RemoteDataService {
             let controllerSettingsDatumIsEffectivelyEquivalent = TControllerSettingsDatum.areEffectivelyEquivalent(old: lastControllerSettingsDatum, new: controllerSettingsDatum)
 
             let cgmSettingsDatum = $0.datumCGMSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+            cgmSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             let cgmSettingsDatumIsEffectivelyEquivalent = TCGMSettingsDatum.areEffectivelyEquivalent(old: lastCGMSettingsDatum, new: cgmSettingsDatum)
 
             let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
+            pumpSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
+            pumpSettingsDatum.automatedDelivery = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum)
 
             // Associate the data
@@ -640,7 +643,7 @@ extension TCGMSettingsDatum: EffectivelyEquivalent {
             self.manufacturers == other.manufacturers &&
             self.model == other.model &&
             self.name == other.name &&
-            self.serialNumber == other.serialNumber &&
+//            self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             self.softwareVersion == other.softwareVersion &&
             self.transmitterId == other.transmitterId &&
             self.units == other.units &&
@@ -676,7 +679,7 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
     // All TDatum properties can be ignored for this datum type
     func isEffectivelyEquivalent(to other: TPumpSettingsDatum) -> Bool {
         return self.activeScheduleName == other.activeScheduleName &&
-            self.automatedDelivery == other.automatedDelivery &&
+//            self.automatedDelivery == other.automatedDelivery && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             self.basal == other.basal &&
             self.basalRateSchedule == other.basalRateSchedule &&
             self.basalRateSchedules == other.basalRateSchedules &&
@@ -700,7 +703,7 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
             self.name == other.name &&
             self.overridePresets == other.overridePresets &&
             self.scheduleTimeZoneOffset == other.scheduleTimeZoneOffset &&
-            self.serialNumber == other.serialNumber &&
+//            self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             self.softwareVersion == other.softwareVersion &&
             self.units == other.units
     }

From 98545f004b978457f539d66b2b83af56b266b12c Mon Sep 17 00:00:00 2001
From: Cameron Ingham <camji555@gmail.com>
Date: Tue, 29 Oct 2024 12:47:38 -0700
Subject: [PATCH 18/21] [LOOP-5132] Remove automatedDelivery and serialNumber
 from TPumpSettingsDatum and TCGMSettingsDatum

---
 TidepoolServiceKit/Extensions/StoredSettings.swift | 9 ---------
 TidepoolServiceKit/TidepoolService.swift           | 9 ---------
 2 files changed, 18 deletions(-)

diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift
index 4b9a84f..2051827 100644
--- a/TidepoolServiceKit/Extensions/StoredSettings.swift
+++ b/TidepoolServiceKit/Extensions/StoredSettings.swift
@@ -65,7 +65,6 @@ extension StoredSettings: IdentifiableDatum {
                                       manufacturers: datumCGMManufacturers,
                                       model: datumCGMModel,
                                       name: datumCGMName,
-                                      serialNumber: datumCGMSerialNumber,
                                       softwareVersion: datumCGMSoftwareVersion,
                                       transmitterId: nil,       // TODO: https://tidepool.atlassian.net/browse/LOOP-3929
                                       units: datumCGMUnits,
@@ -82,7 +81,6 @@ extension StoredSettings: IdentifiableDatum {
     func datumPumpSettings(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpSettingsDatum {
         let datum = TPumpSettingsDatum(time: datumTime,
                                        activeScheduleName: datumPumpActiveScheduleName,
-                                       automatedDelivery: datumPumpAutomatedDelivery,
                                        basal: datumPumpBasal,
                                        basalRateSchedules: datumPumpBasalRateSchedules,
                                        bloodGlucoseSafetyLimit: datumPumpBloodGlucoseSafetyLimit,
@@ -102,7 +100,6 @@ extension StoredSettings: IdentifiableDatum {
                                        name: datumPumpName,
                                        overridePresets: datumPumpOverridePresets,
                                        scheduleTimeZoneOffset: datumPumpScheduleTimeZoneOffset,
-                                       serialNumber: datumPumpSerialNumber,
                                        softwareVersion: datumPumpSoftwareVersion,
                                        units: datumPumpUnits)
         let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion)
@@ -145,8 +142,6 @@ extension StoredSettings: IdentifiableDatum {
 
     private var datumCGMName: String? { cgmDevice?.name }
 
-    private var datumCGMSerialNumber: String? { cgmDevice?.localIdentifier }
-
     private var datumCGMSoftwareVersion: String? { cgmDevice?.softwareVersion }
 
     private var datumCGMUnits: TCGMSettingsDatum.Units { .milligramsPerDeciliter }
@@ -155,8 +150,6 @@ extension StoredSettings: IdentifiableDatum {
         return Self.activeScheduleNameDefault
     }
     
-    private var datumPumpAutomatedDelivery: Bool { dosingEnabled }
-    
     private var datumPumpBasal: TPumpSettingsDatum.Basal? {
         guard let maximumBasalRatePerHour = maximumBasalRatePerHour else {
             return nil
@@ -293,8 +286,6 @@ extension StoredSettings: IdentifiableDatum {
         return TimeInterval(seconds: scheduleTimeZone.secondsFromGMT(for: date))
     }
 
-    private var datumPumpSerialNumber: String? { pumpDevice?.localIdentifier }
-
     private var datumPumpSoftwareVersion: String? { pumpDevice?.softwareVersion }
     
     private var datumPumpUnits: TPumpSettingsDatum.Units {
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 699f06b..5514e85 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -468,12 +468,9 @@ extension TidepoolService: RemoteDataService {
             let controllerSettingsDatumIsEffectivelyEquivalent = TControllerSettingsDatum.areEffectivelyEquivalent(old: lastControllerSettingsDatum, new: controllerSettingsDatum)
 
             let cgmSettingsDatum = $0.datumCGMSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
-            cgmSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             let cgmSettingsDatumIsEffectivelyEquivalent = TCGMSettingsDatum.areEffectivelyEquivalent(old: lastCGMSettingsDatum, new: cgmSettingsDatum)
 
             let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)
-            pumpSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
-            pumpSettingsDatum.automatedDelivery = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum)
 
             // Associate the data
@@ -643,7 +640,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent {
             self.manufacturers == other.manufacturers &&
             self.model == other.model &&
             self.name == other.name &&
-//            self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             self.softwareVersion == other.softwareVersion &&
             self.transmitterId == other.transmitterId &&
             self.units == other.units &&
@@ -662,7 +658,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent {
             manufacturers == nil &&
             model == nil &&
             name == nil &&
-            serialNumber == nil &&
             softwareVersion == nil &&
             transmitterId == nil &&
             defaultAlerts == nil &&
@@ -679,7 +674,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
     // All TDatum properties can be ignored for this datum type
     func isEffectivelyEquivalent(to other: TPumpSettingsDatum) -> Bool {
         return self.activeScheduleName == other.activeScheduleName &&
-//            self.automatedDelivery == other.automatedDelivery && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             self.basal == other.basal &&
             self.basalRateSchedule == other.basalRateSchedule &&
             self.basalRateSchedules == other.basalRateSchedules &&
@@ -703,7 +697,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
             self.name == other.name &&
             self.overridePresets == other.overridePresets &&
             self.scheduleTimeZoneOffset == other.scheduleTimeZoneOffset &&
-//            self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130
             self.softwareVersion == other.softwareVersion &&
             self.units == other.units
     }
@@ -711,7 +704,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
     // Ignore units as they are always specified
     var isEffectivelyEmpty: Bool {
         return activeScheduleName == nil &&
-            automatedDelivery == nil &&
             basal == nil &&
             basalRateSchedule == nil &&
             basalRateSchedules == nil &&
@@ -735,7 +727,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent {
             name == nil &&
             overridePresets == nil &&
             scheduleTimeZoneOffset == nil &&
-            serialNumber == nil &&
             softwareVersion == nil
     }
 }

From 8c7256364eb32e9ec9627baffa8093d9309aae2c Mon Sep 17 00:00:00 2001
From: Cameron Ingham <camji555@gmail.com>
Date: Tue, 29 Oct 2024 12:57:57 -0700
Subject: [PATCH 19/21] [LOOP-5132] Remove automatedDelivery and serialNumber
 from TPumpSettingsDatum and TCGMSettingsDatum

---
 TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift
index 8e33ddf..0d985eb 100644
--- a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift
+++ b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift
@@ -78,7 +78,6 @@ class StoredSettingsTests: XCTestCase {
   "payload" : {
     "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E"
   },
-  "serialNumber" : "CGM Local Identifier",
   "softwareVersion" : "CGM Software Version",
   "time" : "2020-05-14T22:48:15.000Z",
   "timezone" : "America/Los_Angeles",
@@ -95,7 +94,6 @@ class StoredSettingsTests: XCTestCase {
         XCTAssertEqual(String(data: data, encoding: .utf8), """
 {
   "activeSchedule" : "Default",
-  "automatedDelivery" : true,
   "basal" : {
     "rateMaximum" : {
       "units" : "Units/hour",
@@ -231,7 +229,6 @@ class StoredSettingsTests: XCTestCase {
   "payload" : {
     "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E"
   },
-  "serialNumber" : "Pump Local Identifier",
   "softwareVersion" : "Pump Software Version",
   "time" : "2020-05-14T22:48:15.000Z",
   "timezone" : "America/Los_Angeles",

From 342c8e9e07e90559a84722daaa114fd3aee26595 Mon Sep 17 00:00:00 2001
From: Nathaniel Hamming <nate@hmtconsulting.org>
Date: Wed, 30 Oct 2024 16:31:41 -0300
Subject: [PATCH 20/21] [PAL-818] plugin dependency (#112)

* do not allow deleting of the service when it is a dependency

* pass allowDebugFeatures to service
---
 TidepoolService.xcodeproj/project.pbxproj     |  4 ++++
 TidepoolServiceKit/TidepoolService.swift      |  8 +++++++-
 .../Extensions/EnvironmentValues.swift        | 20 +++++++++++++++++++
 TidepoolServiceKitUI/SettingsView.swift       | 16 ++++++++++-----
 TidepoolServiceKitUI/TidepoolService+UI.swift | 12 +++++------
 5 files changed, 48 insertions(+), 12 deletions(-)
 create mode 100644 TidepoolServiceKitUI/Extensions/EnvironmentValues.swift

diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj
index 76a25dd..ed7e8ac 100644
--- a/TidepoolService.xcodeproj/project.pbxproj
+++ b/TidepoolService.xcodeproj/project.pbxproj
@@ -63,6 +63,7 @@
 		A9E8C611272C76A500016E2E /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8C610272C76A500016E2E /* TimeInterval.swift */; };
 		A9F9F317271A046E00D19374 /* StoredCarbEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F9F316271A046E00D19374 /* StoredCarbEntry.swift */; };
 		A9F9F319271A05B100D19374 /* IdentifiableHKDatum.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */; };
+		B40B20CC2CD2AC600027BF35 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */; };
 		C110888F2A39149100BA4898 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C110888E2A39149100BA4898 /* BuildDetails.swift */; };
 		C124239D2A58771A00EAC89E /* TidepoolKit in Frameworks */ = {isa = PBXBuildFile; productRef = C124239C2A58771A00EAC89E /* TidepoolKit */; };
 		C12E4BBA288F2215009C98A2 /* TidepoolServiceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; platformFilter = ios; };
@@ -224,6 +225,7 @@
 		A9E8C610272C76A500016E2E /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = "<group>"; };
 		A9F9F316271A046E00D19374 /* StoredCarbEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCarbEntry.swift; sourceTree = "<group>"; };
 		A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableHKDatum.swift; sourceTree = "<group>"; };
+		B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
 		C110888E2A39149100BA4898 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
 		C12522E1298309B5006EA1CD /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1317D4129830A0800625B94 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -482,6 +484,7 @@
 		C1D0B62A29848BD90098D215 /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */,
 				C1D0B62B29848BEB0098D215 /* Image.swift */,
 				C1C9414529F0CB21008D3E05 /* UIImage.swift */,
 			);
@@ -806,6 +809,7 @@
 				A97651762421AA11002EB5D4 /* OSLog.swift in Sources */,
 				A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */,
 				A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */,
+				B40B20CC2CD2AC600027BF35 /* EnvironmentValues.swift in Sources */,
 				A9DAAD6F22E7EA9700E76C9F /* NibLoadable.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 5514e85..7da1509 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -60,7 +60,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
 
     public let tapi: TAPI = TAPI(clientId: BuildDetails.default.tidepoolServiceClientId, redirectURL: BuildDetails.default.tidepoolServiceRedirectURL)
 
-    public private (set) var error: Error?
+    public private(set) var error: Error?
 
     private let id: String
 
@@ -77,6 +77,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     private let tidepoolKitLog = OSLog(category: "TidepoolKit")
 
     private var deviceLogUploader: DeviceLogUploader?
+    
+    public var isDependency: Bool = false
 
     private func setDeviceLogUploaderDelegate() async {
         await deviceLogUploader?.setDelegate(remoteDataServiceDelegate)
@@ -137,6 +139,10 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject {
     }
 
     public var isOnboarded = false   // No distinction between created and onboarded
+    
+    public func markAsDepedency(_ isDependency: Bool) {
+        self.isDependency = isDependency
+    }
 
     @Published public var session: TSession?
 
diff --git a/TidepoolServiceKitUI/Extensions/EnvironmentValues.swift b/TidepoolServiceKitUI/Extensions/EnvironmentValues.swift
new file mode 100644
index 0000000..56b465b
--- /dev/null
+++ b/TidepoolServiceKitUI/Extensions/EnvironmentValues.swift
@@ -0,0 +1,20 @@
+//
+//  EnvironmentValues.swift
+//  TidepoolService
+//
+//  Created by Nathaniel Hamming on 2024-10-30.
+//  Copyright © 2024 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+
+private struct AllowDebugFeaturesKey: EnvironmentKey {
+    static let defaultValue: Bool = false
+}
+
+public extension EnvironmentValues {
+    var allowDebugFeatures: Bool {
+        get { self[AllowDebugFeaturesKey.self] }
+        set { self[AllowDebugFeaturesKey.self] = newValue }
+    }
+}
diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift
index bacffd3..3a61ac8 100644
--- a/TidepoolServiceKitUI/SettingsView.swift
+++ b/TidepoolServiceKitUI/SettingsView.swift
@@ -11,7 +11,8 @@ import TidepoolKit
 import TidepoolServiceKit
 
 public struct SettingsView: View {
-
+    @Environment(\.allowDebugFeatures) var allowDebugFeatures
+    
     @State private var isEnvironmentActionSheetPresented = false
     @State private var showingDeletionConfirmation = false
 
@@ -28,7 +29,12 @@ public struct SettingsView: View {
     private let onboarding: Bool
 
     var isLoggedIn: Bool {
-        return service.session != nil
+        service.session != nil
+    }
+    
+    var canDeleteService: Bool {
+        guard !allowDebugFeatures else { return true }
+        return !service.isDependency
     }
 
     public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?, onboarding: Bool)
@@ -97,11 +103,11 @@ public struct SettingsView: View {
                             .padding()
                         }
                         Spacer()
-                        if isLoggedIn && !onboarding {
+                        if isLoggedIn && !onboarding && canDeleteService {
                             deleteServiceButton
-                        } else if isLoggedIn {
+                        } else if isLoggedIn && onboarding {
                             continueButton
-                        } else {
+                        } else if !isLoggedIn {
                             loginButton
                         }
                     }
diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift
index 06e3d2c..39634d7 100644
--- a/TidepoolServiceKitUI/TidepoolService+UI.swift
+++ b/TidepoolServiceKitUI/TidepoolService+UI.swift
@@ -34,7 +34,7 @@ extension TidepoolService: @retroactive ServiceUI {
         UIImage(frameworkImage: "Tidepool Logo")
     }
 
-    public static func setupViewController(pluginHost: PluginHost, onboarding: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> {
+    public static func setupViewController(pluginHost: PluginHost, onboarding: Bool, allowDebugFeatures: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> {
 
         let navController = ServiceNavigationController()
         navController.isNavigationBarHidden = true
@@ -58,7 +58,7 @@ extension TidepoolService: @retroactive ServiceUI {
                 Task {
                     await navController.notifyComplete()
                 }
-            }, onboarding: onboarding)
+            }, onboarding: onboarding).environment(\.allowDebugFeatures, allowDebugFeatures)
 
             let hostingController = await UIHostingController(rootView: settingsView)
             await navController.pushViewController(hostingController, animated: false)
@@ -67,11 +67,11 @@ extension TidepoolService: @retroactive ServiceUI {
         return .userInteractionRequired(navController)
     }
 
-    public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult<ServiceViewController, ServiceUI> {
-        return setupViewController(pluginHost: pluginHost, onboarding: false)
+    public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost, allowDebugFeatures: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> {
+        return setupViewController(pluginHost: pluginHost, onboarding: false, allowDebugFeatures: allowDebugFeatures)
     }
 
-    public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController {
+    public func settingsViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> ServiceViewController {
 
         let navController = ServiceNavigationController()
         navController.isNavigationBarHidden = true
@@ -92,7 +92,7 @@ extension TidepoolService: @retroactive ServiceUI {
                 Task {
                     await navController.notifyComplete()
                 }
-            }, onboarding: false)
+            }, onboarding: false).environment(\.allowDebugFeatures, allowDebugFeatures)
 
             let hostingController = await UIHostingController(rootView: settingsView)
             await navController.pushViewController(hostingController, animated: false)

From 2cd32ca193eedd4fb4ed769679f36f1a01126669 Mon Sep 17 00:00:00 2001
From: Pete Schwamb <pete@schwamb.net>
Date: Thu, 31 Oct 2024 11:08:40 -0500
Subject: [PATCH 21/21] Fix deletion bug with manual injection bolus

---
 TidepoolServiceKit/Extensions/DoseEntry.swift | 8 +++++---
 TidepoolServiceKit/TidepoolService.swift      | 4 ++++
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift
index 9448fe2..e8e9b21 100644
--- a/TidepoolServiceKit/Extensions/DoseEntry.swift
+++ b/TidepoolServiceKit/Extensions/DoseEntry.swift
@@ -301,9 +301,11 @@ extension DoseEntry {
         case .basal:
             return [datumSelector(for: TScheduledBasalDatum.self)]
         case .bolus:
-            if manuallyEntered {
-                return [datumSelector(for: TInsulinDatum.self)]
-            } else if automatic != true {
+            // TODO: revert to using .insulin datum type once fully supported in Tidepool frontend
+//            if manuallyEntered {
+//                return [datumSelector(for: TInsulinDatum.self)]
+//            } else if automatic != true {
+            if automatic != true {
                 return [datumSelector(for: TNormalBolusDatum.self)]
             } else {
                 return [datumSelector(for: TAutomatedBolusDatum.self)]
diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift
index 7da1509..f41ebdf 100644
--- a/TidepoolServiceKit/TidepoolService.swift
+++ b/TidepoolServiceKit/TidepoolService.swift
@@ -357,6 +357,10 @@ extension TidepoolService: RemoteDataService {
             throw TidepoolServiceError.configuration
         }
 
+        guard !created.isEmpty || !deleted.isEmpty else {
+            return
+        }
+
         // Syncidentifiers may be changed
         let annotatedCreated = try await annotateDoses(created)
         let _ = try await createData(annotatedCreated.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) })