Aidan Lane 9 месяцев назад
Родитель
Сommit
db32e57331

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.5.1
-APP_DEV_VERSION = 0.5.1.10
+APP_DEV_VERSION = 0.5.1.18
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 1ea5e384c88f4ff51c7679fea4e17fe13c279d40
+Subproject commit bd52ae898a59a05421b0f860e472b1d5aeae7cdc

+ 16 - 16
Trio.xcodeproj/project.pbxproj

@@ -5037,7 +5037,7 @@
 				ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -5052,7 +5052,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).LiveActivity";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
@@ -5071,7 +5071,7 @@
 				ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -5086,7 +5086,7 @@
 					"@executable_path/../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).LiveActivity";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
@@ -5105,7 +5105,7 @@
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -5120,7 +5120,7 @@
 					"@executable_path/../../../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp.TrioWatchComplication";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = watchos;
@@ -5142,7 +5142,7 @@
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -5157,7 +5157,7 @@
 					"@executable_path/../../../../Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp.TrioWatchComplication";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = watchos;
@@ -5178,7 +5178,7 @@
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_ASSET_PATHS = "\"Trio Watch App Extension/Preview Content\"";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
@@ -5193,7 +5193,7 @@
 					"@executable_path/Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = watchos;
@@ -5215,7 +5215,7 @@
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_ASSET_PATHS = "\"Trio Watch App Extension/Preview Content\"";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
@@ -5230,7 +5230,7 @@
 					"@executable_path/Frameworks",
 				);
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = watchos;
@@ -5249,13 +5249,13 @@
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
 				GENERATE_INFOPLIST_FILE = YES;
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).TrioWatchAppTests";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = watchos;
@@ -5275,13 +5275,13 @@
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = "$(APP_BUILD_NUMBER)";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
 				GENERATE_INFOPLIST_FILE = YES;
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
-				MARKETING_VERSION = 1.0;
+				MARKETING_VERSION = "$(APP_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).TrioWatchAppTests";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SDKROOT = watchos;

+ 0 - 12
Trio/Resources/InfoPlist.xcstrings

@@ -457,18 +457,6 @@
         }
       }
     },
-    "NSCalendarsFullAccessUsageDescription" : {
-      "comment" : "Privacy - Calendars Full Access Usage Description",
-      "extractionState" : "extracted_with_value",
-      "localizations" : {
-        "en" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
-          }
-        }
-      }
-    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

+ 11 - 9
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -284,15 +284,17 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             return
         }
 
-        let backfillGlucose = newGlucose.filter { $0.dateString <= syncDate }
-        if backfillGlucose.isNotEmpty {
-            debug(.deviceManager, "Backfilling glucose...")
-            do {
-                try await glucoseStorage.storeGlucose(backfillGlucose)
-            } catch {
-                debug(.deviceManager, "Unable to backfill glucose: \(error)")
-            }
-        }
+        // TODO: Fix backfill logic https://github.com/nightscout/Trio/issues/737
+        /*
+         let backfillGlucose = newGlucose.filter { $0.dateString <= syncDate }
+         if backfillGlucose.isNotEmpty {
+             debug(.deviceManager, "Backfilling glucose...")
+             do {
+                 try await glucoseStorage.storeGlucose(backfillGlucose)
+             } catch {
+                 debug(.deviceManager, "Unable to backfill glucose: \(error)")
+             }
+         }*/
 
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)

+ 4 - 1
Trio/Sources/Helpers/Formatters.swift

@@ -32,6 +32,7 @@ extension Formatter {
     static let decimalFormatterWithTwoFractionDigits: NumberFormatter = {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
+        formatter.locale = .current
         formatter.maximumFractionDigits = 2
         return formatter
     }()
@@ -51,6 +52,7 @@ extension Formatter {
     static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
+        formatter.locale = .current
         formatter.maximumFractionDigits = 1
         return formatter
     }()
@@ -69,6 +71,7 @@ extension Formatter {
 
         switch units {
         case .mmolL:
+            formatter.locale = .current
             formatter.minimumFractionDigits = 1
             formatter.maximumFractionDigits = 1
         case .mgdL:
@@ -81,9 +84,9 @@ extension Formatter {
     static let bolusFormatter: NumberFormatter = {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
+        formatter.locale = .current
         formatter.minimumIntegerDigits = 0
         formatter.maximumFractionDigits = 2
-        formatter.decimalSeparator = "."
         return formatter
     }()
 

+ 66 - 16
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -6430,6 +6430,9 @@
         }
       }
     },
+    "%@ " : {
+
+    },
     "%@  %@" : {
       "localizations" : {
         "bg" : {
@@ -7752,6 +7755,7 @@
     },
     "%@ U" : {
       "comment" : "Number of units insulin delivered",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -8832,9 +8836,6 @@
         }
       }
     },
-    "%lld h" : {
-
-    },
     "%lld hr" : {
       "localizations" : {
         "bg" : {
@@ -20557,17 +20558,6 @@
         }
       }
     },
-    "A external bolus of %@ U of insulin was recorded." : {
-      "extractionState" : "stale",
-      "localizations" : {
-        "fr" : {
-          "stringUnit" : {
-            "state" : "translated",
-            "value" : "Un bolus externe de %@ U d'insuline a été enregistré."
-          }
-        }
-      }
-    },
     "A few important notes…" : {
       "localizations" : {
         "bg" : {
@@ -37421,7 +37411,14 @@
       }
     },
     "An external bolus of %@ U of insulin was recorded." : {
-
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Un bolus externe de %@ U d'insuline a été enregistré."
+          }
+        }
+      }
     },
     "An unknown authentication error occurred. Please try again." : {
       "localizations" : {
@@ -39341,6 +39338,7 @@
       }
     },
     "Applying ${carbQuantity} at ${dateAdded}" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -62240,6 +62238,16 @@
         }
       }
     },
+    "Confirm Before logging" : {
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Confirmer avant d’enregistrer"
+          }
+        }
+      }
+    },
     "Confirm Bolus Faster" : {
       "localizations" : {
         "bg" : {
@@ -115063,6 +115071,16 @@
         }
       }
     },
+    "If toggled, you will need to confirm before logging" : {
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Si coché, vous devez confirmer avant l’enregistrement"
+          }
+        }
+      }
+    },
     "If using Dynamic ISF (with Sigmoid), overriding your ISF will adjust the ISF used at your glucose target which extends to the ISF used at other glucose. Overriding your glucose target will change glucose level your ISF will be set to your profile ISF. Both of these can be combined in a single Override." : {
       "localizations" : {
         "bg" : {
@@ -116348,6 +116366,7 @@
       }
     },
     "Immediately applying ${carbQuantity} at ${dateAdded}" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -116665,6 +116684,16 @@
         }
       }
     },
+    "Immediately Log ${carbQuantity} at ${dateAdded}" : {
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Enregistrer immédiatement ${carbQuantity} à ${dateAdded}"
+          }
+        }
+      }
+    },
     "Import Error" : {
       "comment" : "Import Error HeadlineImport Error Headline",
       "extractionState" : "manual",
@@ -128247,6 +128276,16 @@
         }
       }
     },
+    "Log ${carbQuantity} at ${dateAdded}" : {
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Enregistrer ${carbQuantity} à${dateAdded}"
+          }
+        }
+      }
+    },
     "Log Carbs" : {
       "comment" : "Button Label to Log Carbs on Watch",
       "localizations" : {
@@ -167848,6 +167887,7 @@
       }
     },
     "Quantity fat" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -167953,6 +167993,9 @@
         }
       }
     },
+    "Quantity Fat" : {
+
+    },
     "Quantity of carbs in g" : {
       "localizations" : {
         "bg" : {
@@ -202258,7 +202301,14 @@
       }
     },
     "The external bolus cannot be larger than 3 x the pump setting max bolus (%@)." : {
-
+      "localizations" : {
+        "fr" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Le bolus externe ne peut pas être supérieur à 3 x le réglage maximum de l'bolus de la pompe (%@)."
+          }
+        }
+      }
     },
     "The Fat and Protein Delay setting defines the time between when you log fat and protein and when the system starts delivering insulin for the Fat-Protein Unit Carb Equivalents (FPUs)." : {
       "localizations" : {

+ 49 - 18
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -9,6 +9,8 @@ extension Settings {
         var latestVersion: String?
         var isUpdateAvailable: Bool
         var isBlacklisted: Bool
+        var latestDevVersion: String?
+        var isDevUpdateAvailable: Bool
     }
 
     struct RootView: BaseView {
@@ -27,7 +29,9 @@ extension Settings {
         @State private var versionInfo = VersionInfo(
             latestVersion: nil,
             isUpdateAvailable: false,
-            isBlacklisted: false
+            isBlacklisted: false,
+            latestDevVersion: nil,
+            isDevUpdateAvailable: false
         )
         @State private var closedLoopDisabled = true
 
@@ -40,12 +44,13 @@ extension Settings {
         }
 
         @ViewBuilder var versionInfoView: some View {
-            let latestVersion = versionInfo.latestVersion
-            if let version = latestVersion {
-                let updateColor: Color = versionInfo.isUpdateAvailable ? .orange : .green
-                let versionIconName = versionInfo.isUpdateAvailable ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
+            VStack(alignment: .leading, spacing: 4) {
+                // Main version info
+                if let version = versionInfo.latestVersion {
+                    let updateColor: Color = versionInfo.isUpdateAvailable ? .orange : .green
+                    let versionIconName = versionInfo
+                        .isUpdateAvailable ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
 
-                VStack(alignment: .leading, spacing: 4) {
                     HStack {
                         Text("Latest version: \(version)")
                             .font(.footnote)
@@ -62,11 +67,33 @@ extension Settings {
                                 .foregroundColor(.red)
                         }
                     }
+                } else {
+                    Text("Latest version: Fetching...")
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                }
+
+                // Show latest dev version on any branch except main
+                let buildDetails = BuildDetails.shared
+                if buildDetails.trioBranch != "main" {
+                    if let devVersion = versionInfo.latestDevVersion {
+                        let devUpdateColor: Color = versionInfo.isDevUpdateAvailable ? .orange : .secondary
+                        let devVersionIconName = versionInfo.isDevUpdateAvailable ? "arrow.up.circle.fill" : "hammer.fill"
+
+                        HStack {
+                            Text("Latest dev: \(devVersion)")
+                                .font(.footnote)
+                                .foregroundColor(devUpdateColor)
+                            Image(systemName: devVersionIconName)
+                                .font(.footnote)
+                                .foregroundColor(devUpdateColor)
+                        }
+                    } else {
+                        Text("Latest dev: Fetching...")
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                    }
                 }
-            } else {
-                Text("Latest version: Fetching...")
-                    .font(.footnote)
-                    .foregroundColor(.secondary)
             }
         }
 
@@ -366,14 +393,18 @@ extension Settings {
             .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
             .screenNavigation(self)
             .onAppear {
-                AppVersionChecker.shared.refreshVersionInfo { _, latestVersion, isNewer, isBlacklisted in
-                    let updateAvailable = isNewer
-                    DispatchQueue.main.async {
-                        versionInfo = VersionInfo(
-                            latestVersion: latestVersion,
-                            isUpdateAvailable: updateAvailable,
-                            isBlacklisted: isBlacklisted
-                        )
+                Task { @MainActor in
+                    let (_, latestVersion, isNewer, isBlacklisted) = await AppVersionChecker.shared.refreshVersionInfo()
+                    versionInfo.latestVersion = latestVersion
+                    versionInfo.isUpdateAvailable = isNewer
+                    versionInfo.isBlacklisted = isBlacklisted
+                    
+                    // Fetch dev version if not on main branch
+                    let buildDetails = BuildDetails.shared
+                    if buildDetails.trioBranch != "main" {
+                        let (devVersion, isDevNewer) = await AppVersionChecker.shared.checkForNewDevVersion()
+                        versionInfo.latestDevVersion = devVersion
+                        versionInfo.isDevUpdateAvailable = isDevNewer
                     }
                 }
             }

+ 8 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -121,7 +121,10 @@ struct BolusStatsView: View {
         }
         .onAppear {
             scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
-            updateCalculatedValues()
+            // Delay the initial update to ensure scroll position has been processed
+            DispatchQueue.main.async {
+                updateCalculatedValues()
+            }
         }
         .onChange(of: scrollPosition) {
             updateTimer.scheduleUpdate {
@@ -131,7 +134,10 @@ struct BolusStatsView: View {
         .onChange(of: selectedInterval) {
             Task {
                 scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
-                updateCalculatedValues()
+                // Use async dispatch to ensure scroll position is updated before calculating values
+                await MainActor.run {
+                    updateCalculatedValues()
+                }
             }
         }
     }

+ 11 - 5
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -75,8 +75,11 @@ struct TotalDailyDoseChart: View {
         }
         .onAppear {
             scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
-            updateAverages()
-            updateTotalDoses()
+            // Delay the initial update to ensure scroll position has been processed
+            DispatchQueue.main.async {
+                updateAverages()
+                updateTotalDoses()
+            }
         }
         .onChange(of: scrollPosition) {
             updateTimer.scheduleUpdate {
@@ -89,9 +92,12 @@ struct TotalDailyDoseChart: View {
         .onChange(of: selectedInterval) {
             Task {
                 scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
-                updateAverages()
-                if selectedInterval == .day {
-                    updateTotalDoses()
+                // Use async dispatch to ensure scroll position is updated before calculating averages
+                await MainActor.run {
+                    updateAverages()
+                    if selectedInterval == .day {
+                        updateTotalDoses()
+                    }
                 }
             }
         }

+ 8 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -100,7 +100,10 @@ struct MealStatsView: View {
         }
         .onAppear {
             scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
-            updateAverages()
+            // Delay the initial update to ensure scroll position has been processed
+            DispatchQueue.main.async {
+                updateAverages()
+            }
         }
         .onChange(of: scrollPosition) {
             updateTimer.scheduleUpdate {
@@ -110,7 +113,10 @@ struct MealStatsView: View {
         .onChange(of: selectedInterval) {
             Task {
                 scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
-                updateAverages()
+                // Use async dispatch to ensure scroll position is updated before calculating averages
+                await MainActor.run {
+                    updateAverages()
+                }
             }
         }
     }

+ 8 - 19
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -20,20 +20,6 @@ struct ForecastChart: View {
             )) // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
     }
 
-    private var glucoseFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-
-        if state.units == .mmolL {
-            formatter.maximumFractionDigits = 1
-            formatter.minimumFractionDigits = 1
-            formatter.roundingMode = .halfUp
-        } else {
-            formatter.maximumFractionDigits = 0
-        }
-        return formatter
-    }
-
     private var selectedGlucose: GlucoseStored? {
         guard let selection = selection else { return nil }
         let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
@@ -73,8 +59,11 @@ struct ForecastChart: View {
 
             HStack {
                 Image(systemName: "syringe.fill")
-                Text("\(state.amount.description) U")
+                Text(
+                    "\(Formatter.bolusFormatter.string(from: state.amount as NSNumber) ?? state.amount.description) "
+                ) + Text(String(localized: "U", comment: "Insulin unit"))
             }
+
             .font(.footnote)
             .foregroundStyle(.blue)
             .padding(8)
@@ -185,19 +174,19 @@ struct ForecastChart: View {
                 HStack(spacing: 10) {
                     HStack(spacing: 4) {
                         Image(systemName: "circle.fill").foregroundStyle(Color.insulin)
-                        Text("IOB").foregroundStyle(Color.secondary)
+                        Text(String(localized: "IOB")).foregroundStyle(Color.secondary)
                     }
                     HStack(spacing: 4) {
                         Image(systemName: "circle.fill").foregroundStyle(Color.uam)
-                        Text("UAM").foregroundStyle(Color.secondary)
+                        Text(String(localized: "UAM")).foregroundStyle(Color.secondary)
                     }
                     HStack(spacing: 4) {
                         Image(systemName: "circle.fill").foregroundStyle(Color.zt)
-                        Text("ZT").foregroundStyle(Color.secondary)
+                        Text(String(localized: "ZT")).foregroundStyle(Color.secondary)
                     }
                     HStack(spacing: 4) {
                         Image(systemName: "circle.fill").foregroundStyle(Color.orange)
-                        Text("COB").foregroundStyle(Color.secondary)
+                        Text(String(localized: "COB")).foregroundStyle(Color.secondary)
                     }
                 }.font(.caption2)
             }

+ 308 - 153
Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift

@@ -1,78 +1,88 @@
 import UIKit
 
-/// AppVersionChecker is a singleton responsible for checking the app's version status.
-/// It fetches version data from remote sources (GitHub), caches the results, and notifies the user
-/// if an update is available or if the current version is blacklisted.
+// AppVersionChecker is a singleton responsible for checking the app's version status.
+// It fetches version data from remote sources (GitHub), caches the results, and notifies the user
+// if an update is available or if the current version is blacklisted.
 final class AppVersionChecker {
-    /// Shared singleton instance.
+    // Shared singleton instance.
     static let shared = AppVersionChecker()
 
-    /// Private initializer to enforce the singleton pattern.
+    // Private initializer to enforce the singleton pattern.
     private init() {}
 
     // MARK: - Persisted Properties
 
-    /// Cached app version for which data was last fetched.
+    // Cached app version for which data was last fetched.
     @Persisted(key: "cachedForVersion") private var cachedForVersion: String? = nil
-    /// The latest version fetched from GitHub.
+    // The latest version fetched from GitHub.
     @Persisted(key: "latestVersion") private var persistedLatestVersion: String? = nil
-    /// The date when the latest version was checked.
+    // The date when the latest version was checked.
     @Persisted(key: "latestVersionChecked") private var latestVersionChecked: Date? = .distantPast
-    /// Boolean flag indicating whether the current version is blacklisted.
+    // Boolean flag indicating whether the current version is blacklisted.
     @Persisted(key: "currentVersionBlackListed") private var currentVersionBlackListed: Bool = false
-    /// Timestamp for the last time a blacklist notification was shown.
+    // Timestamp for the last time a blacklist notification was shown.
     @Persisted(key: "lastBlacklistNotificationShown") private var lastBlacklistNotificationShown: Date? = .distantPast
-    /// Timestamp for the last time a version update notification was shown.
+    // Timestamp for the last time a version update notification was shown.
     @Persisted(key: "lastVersionUpdateNotificationShown") private var lastVersionUpdateNotificationShown: Date? = .distantPast
-    /// Timestamp for the last time an expiration notification was shown.
+    // Timestamp for the last time an expiration notification was shown.
     @Persisted(key: "lastExpirationNotificationShown") private var lastExpirationNotificationShown: Date? = .distantPast
 
+    // Dev version properties
+    // Cached app version for which dev data was last fetched.
+    @Persisted(key: "cachedForDevVersion") private var cachedForDevVersion: String? = nil
+    // The latest dev version fetched from GitHub.
+    @Persisted(key: "latestDevVersion") private var persistedLatestDevVersion: String? = nil
+    // The date when the latest dev version was checked.
+    @Persisted(key: "latestDevVersionChecked") private var latestDevVersionChecked: Date? = .distantPast
+
     // MARK: - Nested Types
 
-    /// GitHubDataType defines the type of data to fetch from GitHub for version checking.
+    // GitHubDataType defines the type of data to fetch from GitHub for version checking.
     private enum GitHubDataType {
-        /// The configuration file containing version information.
+        // The configuration file containing version information.
         case versionConfig
-        /// The JSON file listing blacklisted versions.
+        // The configuration file containing dev version information.
+        case devVersionConfig
+        // The JSON file listing blacklisted versions.
         case blacklistedVersions
 
-        /// Returns the URL string associated with the data type.
+        // Returns the URL string associated with the data type.
         var url: String {
             switch self {
             case .versionConfig:
                 return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/main/Config.xcconfig"
+            case .devVersionConfig:
+                return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/dev/Config.xcconfig"
             case .blacklistedVersions:
                 return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/main/blacklisted-versions.json"
             }
         }
     }
 
-    /// Model for decoding the blacklist JSON from GitHub.
+    // Model for decoding the blacklist JSON from GitHub.
     private struct Blacklist: Decodable {
-        /// Array of blacklisted version entries.
+        // Array of blacklisted version entries.
         let blacklistedVersions: [VersionEntry]
     }
 
-    /// Model representing a single version entry in the blacklist.
+    // Model representing a single version entry in the blacklist.
     private struct VersionEntry: Decodable {
-        /// The version string that is blacklisted.
+        // The version string that is blacklisted.
         let version: String
     }
 
     // MARK: - Public Methods
 
-    /**
-     Checks for a new or blacklisted version and presents an alert if necessary.
-
-     This method determines whether there is an update or if the current version is blacklisted.
-     Depending on the result, it displays an alert on the given view controller, ensuring that alerts
-     are not shown too frequently (24 hours for blacklist and 2 weeks for update notifications).
-
-     - Parameter viewController: The UIViewController on which to present any alerts.
-     */
+    // Checks for a new or blacklisted version and presents an alert if necessary.
+    //
+    // This method determines whether there is an update or if the current version is blacklisted.
+    // Depending on the result, it displays an alert on the given view controller, ensuring that alerts
+    // are not shown too frequently (24 hours for blacklist and 2 weeks for update notifications).
+    //
+    // - Parameter viewController: The UIViewController on which to present any alerts.
     func checkAndNotifyVersionStatus(in viewController: UIViewController) {
-        checkForNewVersion { [weak viewController] latestVersion, isNewer, isBlacklisted in
-            guard let vc = viewController else { return }
+        Task { @MainActor in
+            let (latestVersion, isNewer, isBlacklisted) = await checkForNewVersion()
             let now = Date()
 
             // If the current version is blacklisted, show a critical update alert if not shown in the last 24 hours.
@@ -80,7 +90,7 @@ final class AppVersionChecker {
                 let lastShown = self.lastBlacklistNotificationShown ?? .distantPast
                 if now.timeIntervalSince(lastShown) > 86400 { // 24 hours
                     self.showAlert(
-                        on: vc,
+                        on: viewController,
                         title: String(localized: "Update Required", comment: "Title for critical update alert"),
                         message: String(
                             localized: "The current version has a critical issue and should be updated as soon as possible.",
@@ -97,7 +107,7 @@ final class AppVersionChecker {
                 if now.timeIntervalSince(lastShown) > 1_209_600 { // 2 weeks
                     let versionText = latestVersion ?? String(localized: "Unknown", comment: "Fallback text for unknown version")
                     self.showAlert(
-                        on: vc,
+                        on: viewController,
                         title: String(localized: "Update Available", comment: "Title for update available alert"),
                         message: String(
                             localized: "A new version (\(versionText)) is available. It is recommended to update.",
@@ -110,46 +120,163 @@ final class AppVersionChecker {
         }
     }
 
-    /**
-     Refreshes the version information and returns the current state.
-
-     This method triggers a version check (using cached values if valid or fetching fresh data)
-     and then returns the current app version along with the latest version info, a flag indicating
-     whether the latest version is newer, and a flag indicating if the current version is blacklisted.
-
-     - Parameter completion: A closure that receives the following parameters:
-     - currentVersion: The current app version.
-     - latestVersion: The latest version fetched from GitHub (if available).
-     - isNewer: `true` if the fetched version is newer than the current version.
-     - isBlacklisted: `true` if the current version is blacklisted.
-     */
+    // Refreshes the version information and returns the current state (completion handler version).
+    //
+    // This method triggers a version check (using cached values if valid or fetching fresh data)
+    // and then returns the current app version along with the latest version info, a flag indicating
+    // whether the latest version is newer, and a flag indicating if the current version is blacklisted.
+    //
+    // - Parameter completion: A closure that receives the following parameters:
+    // - currentVersion: The current app version.
+    // - latestVersion: The latest version fetched from GitHub (if available).
+    // - isNewer: `true` if the fetched version is newer than the current version.
+    // - isBlacklisted: `true` if the current version is blacklisted.
     func refreshVersionInfo(completion: @escaping (
         String,
         String?,
         Bool,
         Bool
     ) -> Void) {
+        Task {
+            let result = await refreshVersionInfo()
+            completion(result.currentVersion, result.latestVersion, result.isNewer, result.isBlacklisted)
+        }
+    }
+    
+    // Refreshes the version information and returns the current state (async version).
+    //
+    // This method triggers a version check (using cached values if valid or fetching fresh data)
+    // and then returns the current app version along with the latest version info, a flag indicating
+    // whether the latest version is newer, and a flag indicating if the current version is blacklisted.
+    //
+    // - Returns: A tuple containing:
+    // - currentVersion: The current app version.
+    // - latestVersion: The latest version fetched from GitHub (if available).
+    // - isNewer: `true` if the fetched version is newer than the current version.
+    // - isBlacklisted: `true` if the current version is blacklisted.
+    func refreshVersionInfo() async -> (currentVersion: String, latestVersion: String?, isNewer: Bool, isBlacklisted: Bool) {
         let currentVersion = version()
-        checkForNewVersion { latestVersion, isNewer, isBlacklisted in
-            completion(currentVersion, latestVersion, isNewer, isBlacklisted)
+        let (latestVersion, isNewer, isBlacklisted) = await checkForNewVersion()
+        return (currentVersion, latestVersion, isNewer, isBlacklisted)
+    }
+
+    // Checks for the latest dev version with caching and comparison (completion handler version).
+    //
+    // This method attempts to use cached dev version data if it is less than 24 hours old and
+    // corresponds to the current app version. If the cache is invalid or outdated,
+    // it fetches fresh data from GitHub.
+    //
+    // - Parameter completion: A closure that receives:
+    // - latestDevVersion: The latest dev version string (if available).
+    // - isNewer: `true` if the fetched dev version is newer than the current version.
+    func checkForNewDevVersion(completion: @escaping (String?, Bool) -> Void) {
+        Task {
+            let result = await checkForNewDevVersion()
+            completion(result.0, result.1)
         }
     }
+    
+    // Checks for the latest dev version with caching and comparison (async version).
+    //
+    // This method attempts to use cached dev version data if it is less than 24 hours old and
+    // corresponds to the current app version. If the cache is invalid or outdated,
+    // it fetches fresh data from GitHub.
+    //
+    // - Returns: A tuple containing:
+    // - latestDevVersion: The latest dev version string (if available).
+    // - isNewer: `true` if the fetched dev version is newer than the current version.
+    func checkForNewDevVersion() async -> (String?, Bool) {
+        // For dev version, we need to compare against the current dev version, not the main version
+        let currentDevVersion = Bundle.main.object(forInfoDictionaryKey: "AppDevVersion") as? String ?? version()
+        let now = Date()
 
-    // MARK: - Core Version Checking Logic
+        // Retrieve cached values
+        let lastChecked = latestDevVersionChecked ?? .distantPast
+        let cachedVersion = cachedForDevVersion
+        let persistedLatestDev = persistedLatestDevVersion
 
-    /**
-     Checks whether there is a new or blacklisted version.
+        // Use cached data if it is valid (less than 24 hours old) and matches the current version
+        if let cachedVersion = cachedVersion,
+           cachedVersion == currentDevVersion,
+           now.timeIntervalSince(lastChecked) < 24 * 3600,
+           let persistedLatestDev = persistedLatestDev
+        {
+            let isNewer = isVersion(persistedLatestDev, newerThan: currentDevVersion)
+            return (persistedLatestDev, isNewer)
+        }
 
-     This method attempts to use cached version data if it is less than 24 hours old and
-     corresponds to the current app version. If the cache is invalid or outdated,
-     it fetches fresh data from GitHub.
+        // Otherwise, fetch fresh data from GitHub and update the cache
+        return await fetchDevVersionAndUpdateCache(currentVersion: currentDevVersion)
+    }
+
+    // Fetches dev version data from GitHub, updates persisted values, and returns the result.
+    //
+    // - Parameters:
+    // - currentVersion: The current app version.
+    // - Returns: A tuple containing:
+    // - latestDevVersion: The latest dev version string from GitHub (if available).
+    // - isNewer: `true` if the fetched dev version is newer than the current version.
+    private func fetchDevVersionAndUpdateCache(currentVersion: String) async -> (String?, Bool) {
+        let versionData = await fetchData(for: .devVersionConfig)
+        
+        // Parse the dev version from the fetched configuration data
+        let configContents = versionData.flatMap { String(data: $0, encoding: .utf8) }
+        let fetchedDevVersion = configContents.flatMap { self.parseDevVersionFromConfig(contents: $0) }
+
+        #if DEBUG
+            print("AppVersionChecker.fetchDevVersion: Current dev version: \(currentVersion)")
+            print("AppVersionChecker.fetchDevVersion: Fetched dev version: \(fetchedDevVersion ?? "nil")")
+            if let contents = configContents {
+                let lines = contents.split(separator: "\n")
+                for line in lines where line.contains("VERSION") {
+                    print("AppVersionChecker.fetchDevVersion: Config line: \(line)")
+                }
+            }
+        #endif
+
+        // Determine if the fetched dev version is newer than the current version
+        let isNewer = fetchedDevVersion.map {
+            self.isVersion($0, newerThan: currentVersion)
+        } ?? false
+
+        // Update persisted cache
+        self.persistedLatestDevVersion = fetchedDevVersion
+        self.latestDevVersionChecked = Date()
+        self.cachedForDevVersion = currentVersion
+
+        return (fetchedDevVersion, isNewer)
+    }
+
+    // MARK: - Core Version Checking Logic
 
-     - Parameter completion: A closure that receives:
-     - latestVersion: The latest version string (if available).
-     - isNewer: `true` if the fetched version is newer than the current version.
-     - isBlacklisted: `true` if the current version is blacklisted.
-     */
+    // Checks whether there is a new or blacklisted version (completion handler version).
+    //
+    // This method attempts to use cached version data if it is less than 24 hours old and
+    // corresponds to the current app version. If the cache is invalid or outdated,
+    // it fetches fresh data from GitHub.
+    //
+    // - Parameter completion: A closure that receives:
+    // - latestVersion: The latest version string (if available).
+    // - isNewer: `true` if the fetched version is newer than the current version.
+    // - isBlacklisted: `true` if the current version is blacklisted.
     private func checkForNewVersion(completion: @escaping (String?, Bool, Bool) -> Void) {
+        Task {
+            let result = await checkForNewVersion()
+            completion(result.0, result.1, result.2)
+        }
+    }
+    
+    // Checks whether there is a new or blacklisted version (async version).
+    //
+    // This method attempts to use cached version data if it is less than 24 hours old and
+    // corresponds to the current app version. If the cache is invalid or outdated,
+    // it fetches fresh data from GitHub.
+    //
+    // - Returns: A tuple containing:
+    // - latestVersion: The latest version string (if available).
+    // - isNewer: `true` if the fetched version is newer than the current version.
+    // - isBlacklisted: `true` if the current version is blacklisted.
+    private func checkForNewVersion() async -> (String?, Bool, Bool) {
         let currentVersion = version()
         let now = Date()
 
@@ -172,73 +299,87 @@ final class AppVersionChecker {
            let persistedLatest = persistedLatest
         {
             let isNewer = isVersion(persistedLatest, newerThan: currentVersion)
-            completion(persistedLatest, isNewer, isBlacklistedCached)
-            return
+            return (persistedLatest, isNewer, isBlacklistedCached)
         }
 
         // Otherwise, fetch fresh data from GitHub and update the cache.
-        fetchDataAndUpdateCache(currentVersion: currentVersion, completion: completion)
+        return await fetchDataAndUpdateCache(currentVersion: currentVersion)
     }
 
-    /**
-     Fetches version and blacklist data from GitHub, updates persisted values, and invokes the completion handler.
-
-     This method performs two sequential network requests: first for the version configuration and then for the
-     blacklisted versions. After parsing the fetched data and comparing version values, it updates the cache and calls
-     the completion handler with the results.
-
-     - Parameters:
-     - currentVersion: The current app version.
-     - completion: A closure that receives:
-     - latestVersion: The latest version string from GitHub (if available).
-     - isNewer: `true` if the fetched version is newer than the current version.
-     - isBlacklisted: `true` if the current version is blacklisted.
-     */
-    private func fetchDataAndUpdateCache(currentVersion: String, completion: @escaping (String?, Bool, Bool) -> Void) {
-        fetchData(for: .versionConfig) { versionData in
-            self.fetchData(for: .blacklistedVersions) { blacklistData in
-                DispatchQueue.main.async {
-                    // Parse the version from the fetched configuration data.
-                    let fetchedVersion = versionData
-                        .flatMap { String(data: $0, encoding: .utf8) }
-                        .flatMap { self.parseVersionFromConfig(contents: $0) }
-
-                    // Determine if the fetched version is newer than the current version.
-                    let isNewer = fetchedVersion.map {
-                        self.isVersion($0, newerThan: currentVersion)
-                    } ?? false
-
-                    // Determine if the current version is blacklisted.
-                    let isBlacklisted = (try? blacklistData.flatMap {
-                        try JSONDecoder().decode(Blacklist.self, from: $0)
-                    })?.blacklistedVersions
-                        .map(\.version)
-                        .contains(currentVersion) ?? false
-
-                    // Update persisted cache.
-                    self.persistedLatestVersion = fetchedVersion
-                    self.latestVersionChecked = Date()
-                    self.currentVersionBlackListed = isBlacklisted
-                    self.cachedForVersion = currentVersion
-
-                    completion(fetchedVersion, isNewer, isBlacklisted)
-                }
-            }
-        }
+    // Fetches version and blacklist data from GitHub, updates persisted values, and returns the result.
+    //
+    // This method performs two parallel network requests: one for the version configuration and one for the
+    // blacklisted versions. After parsing the fetched data and comparing version values, it updates the cache and
+    // returns the results.
+    //
+    // - Parameters:
+    // - currentVersion: The current app version.
+    // - Returns: A tuple containing:
+    // - latestVersion: The latest version string from GitHub (if available).
+    // - isNewer: `true` if the fetched version is newer than the current version.
+    // - isBlacklisted: `true` if the current version is blacklisted.
+    private func fetchDataAndUpdateCache(currentVersion: String) async -> (String?, Bool, Bool) {
+        // Fetch both data types in parallel
+        async let versionData = fetchData(for: .versionConfig)
+        async let blacklistData = fetchData(for: .blacklistedVersions)
+        
+        let (versionDataResult, blacklistDataResult) = await (versionData, blacklistData)
+        
+        // Parse the version from the fetched configuration data.
+        let fetchedVersion = versionDataResult
+            .flatMap { String(data: $0, encoding: .utf8) }
+            .flatMap { self.parseVersionFromConfig(contents: $0) }
+
+        // Determine if the fetched version is newer than the current version.
+        let isNewer = fetchedVersion.map {
+            self.isVersion($0, newerThan: currentVersion)
+        } ?? false
+
+        // Determine if the current version is blacklisted.
+        let isBlacklisted = (try? blacklistDataResult.flatMap {
+            try JSONDecoder().decode(Blacklist.self, from: $0)
+        })?.blacklistedVersions
+            .map(\.version)
+            .contains(currentVersion) ?? false
+
+        // Update persisted cache.
+        self.persistedLatestVersion = fetchedVersion
+        self.latestVersionChecked = Date()
+        self.currentVersionBlackListed = isBlacklisted
+        self.cachedForVersion = currentVersion
+
+        return (fetchedVersion, isNewer, isBlacklisted)
     }
 
     // MARK: - Data Fetching Helper
 
-    /**
-     Fetches data from GitHub for a specified data type.
-
-     This helper method builds a URL from the provided GitHubDataType and executes a network request.
-     If the request is successful and returns valid data (HTTP status 200), the data is passed to the completion handler.
+    // Fetches data from GitHub for a specified data type.
+    //
+    // This helper method builds a URL from the provided GitHubDataType and executes a network request.
+    // If the request is successful and returns valid data (HTTP status 200), the data is returned.
+    //
+    // - Parameters:
+    // - dataType: The type of GitHub data to fetch (version configuration or blacklisted versions).
+    // - Returns: The fetched data as an optional `Data` object.
+    private func fetchData(for dataType: GitHubDataType) async -> Data? {
+        guard let url = URL(string: dataType.url) else {
+            return nil
+        }
 
-     - Parameters:
-     - dataType: The type of GitHub data to fetch (version configuration or blacklisted versions).
-     - completion: A closure that receives the fetched data as an optional `Data` object.
-     */
+        do {
+            let (data, response) = try await URLSession.shared.data(from: url)
+            guard let httpResponse = response as? HTTPURLResponse,
+                  httpResponse.statusCode == 200
+            else {
+                return nil
+            }
+            return data
+        } catch {
+            return nil
+        }
+    }
+    
+    // Legacy completion handler version for existing code
     private func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) {
         guard let url = URL(string: dataType.url) else {
             completion(nil)
@@ -259,19 +400,17 @@ final class AppVersionChecker {
 
     // MARK: - Helpers
 
-    /**
-     Parses the version string from the contents of a configuration file.
-
-     The method scans each line of the provided content for an occurrence of "APP_VERSION" and then
-     extracts the version number following the "=" delimiter.
-
-     - Parameter contents: A string containing the contents of the configuration file.
-     - Returns: The extracted version string if found; otherwise, `nil`.
-     */
+    // Parses the version string from the contents of a configuration file.
+    //
+    // The method scans each line of the provided content for an occurrence of "APP_VERSION" and then
+    // extracts the version number following the "=" delimiter.
+    //
+    // - Parameter contents: A string containing the contents of the configuration file.
+    // - Returns: The extracted version string if found; otherwise, `nil`.
     private func parseVersionFromConfig(contents: String) -> String? {
         let lines = contents.split(separator: "\n")
         for line in lines {
-            if line.contains("APP_VERSION") {
+            if line.contains("APP_VERSION") && !line.contains("DEV") {
                 let components = line.split(separator: "=").map {
                     $0.trimmingCharacters(in: .whitespacesAndNewlines)
                 }
@@ -283,18 +422,38 @@ final class AppVersionChecker {
         return nil
     }
 
-    /**
-     Compares two version strings to determine if the fetched version is newer than the current version.
-
-     The version strings are split into numeric components and compared sequentially.
-     If any component of the fetched version is greater than its counterpart in the current version,
-     the function returns `true`; if lower, it returns `false`.
+    // Parses the dev version string from the contents of a configuration file.
+    //
+    // The method scans each line of the provided content for an occurrence of "APP_DEV_VERSION" and then
+    // extracts the version number following the "=" delimiter.
+    //
+    // - Parameter contents: A string containing the contents of the configuration file.
+    // - Returns: The extracted dev version string if found; otherwise, `nil`.
+    private func parseDevVersionFromConfig(contents: String) -> String? {
+        let lines = contents.split(separator: "\n")
+        for line in lines {
+            if line.contains("APP_DEV_VERSION") {
+                let components = line.split(separator: "=").map {
+                    $0.trimmingCharacters(in: .whitespacesAndNewlines)
+                }
+                if components.count > 1 {
+                    return components[1]
+                }
+            }
+        }
+        return nil
+    }
 
-     - Parameters:
-     - fetchedVersion: The version string obtained from GitHub.
-     - currentVersion: The current app version.
-     - Returns: `true` if the fetched version is newer than the current version; otherwise, `false`.
-     */
+    // Compares two version strings to determine if the fetched version is newer than the current version.
+    //
+    // The version strings are split into numeric components and compared sequentially.
+    // If any component of the fetched version is greater than its counterpart in the current version,
+    // the function returns `true`; if lower, it returns `false`.
+    //
+    // - Parameters:
+    // - fetchedVersion: The version string obtained from GitHub.
+    // - currentVersion: The current app version.
+    // - Returns: `true` if the fetched version is newer than the current version; otherwise, `false`.
     private func isVersion(_ fetchedVersion: String, newerThan currentVersion: String) -> Bool {
         let fetchedComponents = fetchedVersion.split(separator: ".").map { Int($0) ?? 0 }
         let currentComponents = currentVersion.split(separator: ".").map { Int($0) ?? 0 }
@@ -312,26 +471,22 @@ final class AppVersionChecker {
         return false
     }
 
-    /**
-     Retrieves the current app version from the main bundle.
-
-     - Returns: The current app version as defined in the app's Info.plist under "CFBundleShortVersionString",
-     or `"Unknown"` if not available.
-     */
+    // Retrieves the current app version from the main bundle.
+    //
+    // - Returns: The current app version as defined in the app's Info.plist under "CFBundleShortVersionString",
+    // or `"Unknown"` if not available.
     private func version() -> String {
         Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
     }
 
-    /**
-     Presents an alert on the specified view controller with a given title and message.
-
-     The alert is dispatched to the main thread to ensure UI updates occur correctly.
-
-     - Parameters:
-     - viewController: The UIViewController on which the alert should be presented.
-     - title: The title text for the alert.
-     - message: The body message of the alert.
-     */
+    // Presents an alert on the specified view controller with a given title and message.
+    //
+    // The alert is dispatched to the main thread to ensure UI updates occur correctly.
+    //
+    // - Parameters:
+    // - viewController: The UIViewController on which the alert should be presented.
+    // - title: The title text for the alert.
+    // - message: The body message of the alert.
     private func showAlert(on viewController: UIViewController, title: String, message: String) {
         DispatchQueue.main.async {
             let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)

+ 30 - 1
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -69,6 +69,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             .receive(on: DispatchQueue.global(qos: .background))
             .sink { [weak self] _ in
                 guard let self = self else { return }
+                // Skip if no watch is paired or app not installed
+                guard let session = self.session, session.isPaired, session.isReachable,
+                      session.isWatchAppInstalled else { return }
                 Task {
                     let state = await self.setupWatchState()
                     await self.sendDataToWatch(state)
@@ -82,6 +85,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     private func registerHandlers() {
         coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
+            // Skip if no watch is paired or app not installed
+            guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
             Task {
                 let state = await self.setupWatchState()
                 await self.sendDataToWatch(state)
@@ -91,6 +96,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
+            // Skip if no watch is paired or app not installed
+            guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
             Task {
                 let state = await self.setupWatchState()
                 await self.sendDataToWatch(state)
@@ -106,6 +113,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
         coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
+            // Skip if no watch is paired or app not installed
+            guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
             Task {
                 let state = await self.setupWatchState()
                 await self.sendDataToWatch(state)
@@ -114,6 +123,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
         coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
             guard let self = self else { return }
+            // Skip if no watch is paired or app not installed
+            guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
             Task {
                 let state = await self.setupWatchState()
                 await self.sendDataToWatch(state)
@@ -148,6 +159,17 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     /// Prepares the current state data to be sent to the Watch
     /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
     func setupWatchState() async -> WatchState {
+        // Check if a watch is paired and reachable before doing expensive calculations
+        guard let session = session, session.isPaired, session.isReachable, session.isWatchAppInstalled else {
+            debug(.watchManager, "⌚️❌ Skipping setupWatchState - No Watch is paired or app not installed")
+            return WatchState(date: Date())
+        }
+
+        // Skip if watch session is not activated
+        guard session.activationState == .activated else {
+            debug(.watchManager, "⌚️❌ Skipping setupWatchState - Watch session not activated")
+            return WatchState(date: Date())
+        }
         do {
             // Get NSManagedObjectIDs
             let glucoseIds = try await fetchGlucose()
@@ -527,7 +549,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             {
                 debug(.watchManager, "📱 Watch requested watch state data update.")
                 guard let self = self else { return }
-
+                // Skip if no watch is paired or app not installed
+                guard let session = self.session, session.isPaired, session.isReachable,
+                      session.isWatchAppInstalled else { return }
                 Task {
                     let state = await self.setupWatchState()
                     await self.sendDataToWatch(state)
@@ -1137,6 +1161,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 extension BaseWatchManager: SettingsObserver, PumpSettingsObserver {
     // to update maxBolus
     func pumpSettingsDidChange(_: PumpSettings) {
+        // Skip if no watch is paired or app not installed
+        guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
         Task {
             let state = await self.setupWatchState()
             await self.sendDataToWatch(state)
@@ -1150,6 +1176,9 @@ extension BaseWatchManager: SettingsObserver, PumpSettingsObserver {
         lowGlucose = settingsManager.settings.low
         highGlucose = settingsManager.settings.high
 
+        // Skip if no watch is paired or app not installed
+        guard let session = self.session, session.isPaired, session.isReachable, session.isWatchAppInstalled else { return }
+
         Task {
             let state = await self.setupWatchState()
             await self.sendDataToWatch(state)

+ 11 - 0
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -133,6 +133,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .receive(on: DispatchQueue.global(qos: .background))
             .sink { [weak self] _ in
                 guard let self = self else { return }
+                // Skip if no Garmin devices are connected
+                guard !self.devices.isEmpty else { return }
                 Task {
                     do {
                         let watchState = try await self.setupGarminWatchState()
@@ -160,6 +162,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .filteredByEntityName("OrefDetermination")
             .sink { [weak self] _ in
                 guard let self = self else { return }
+                // Skip if no Garmin devices are connected
+                guard !self.devices.isEmpty else { return }
                 Task {
                     do {
                         let watchState = try await self.setupGarminWatchState()
@@ -180,6 +184,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .filteredByEntityName("GlucoseStored")
             .sink { [weak self] _ in
                 guard let self = self else { return }
+                // Skip if no Garmin devices are connected
+                guard !self.devices.isEmpty else { return }
                 Task {
                     do {
                         let watchState = try await self.setupGarminWatchState()
@@ -219,6 +225,11 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Builds a `GarminWatchState` reflecting the latest glucose, trend, delta, eventual BG, ISF, IOB, and COB.
     /// - Returns: A `GarminWatchState` containing the most recent device- and therapy-related info.
     func setupGarminWatchState() async throws -> GarminWatchState {
+        // Skip expensive calculations if no Garmin devices are connected
+        guard !devices.isEmpty else {
+            debug(.watchManager, "⌚️❌ Skipping setupGarminWatchState - No Garmin devices connected")
+            return GarminWatchState()
+        }
         do {
             // Get Glucose IDs
             let glucoseIds = try await fetchGlucose()

+ 5 - 5
Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift

@@ -19,7 +19,7 @@ import Swinject
     ) var carbQuantity: Double?
 
     @Parameter(
-        title: "Quantity fat",
+        title: "Quantity Fat",
         description: "Quantity of fat in g",
         default: 0.0,
         inclusiveRange: (0, 200),
@@ -46,21 +46,21 @@ import Swinject
     ) var note: String?
 
     @Parameter(
-        title: "Confirm Before applying",
-        description: "If toggled, you will need to confirm before applying",
+        title: "Confirm Before logging",
+        description: "If toggled, you will need to confirm before logging",
         default: true
     ) var confirmBeforeApplying: Bool
 
     static var parameterSummary: some ParameterSummary {
         When(\.$confirmBeforeApplying, .equalTo, true, {
-            Summary("Applying \(\.$carbQuantity) at \(\.$dateAdded)") {
+            Summary("Log \(\.$carbQuantity) at \(\.$dateAdded)") {
                 \.$fatQuantity
                 \.$proteinQuantity
                 \.$note
                 \.$confirmBeforeApplying
             }
         }, otherwise: {
-            Summary("Immediately applying \(\.$carbQuantity) at \(\.$dateAdded)") {
+            Summary("Immediately Log \(\.$carbQuantity) at \(\.$dateAdded)") {
                 \.$fatQuantity
                 \.$proteinQuantity
                 \.$note