Browse Source

Merge branch 'dev' of github.com:nightscout/Trio-dev into blAccess

Deniz Cengiz 1 năm trước cách đây
mục cha
commit
f8bd4c0113
33 tập tin đã thay đổi với 6443 bổ sung474 xóa
  1. 12 0
      Model/Helper/NSPredicates.swift
  2. 748 0
      Model/JSONImporter.swift
  3. 66 10
      Trio.xcodeproj/project.pbxproj
  4. 11 0
      Trio/Sources/APS/APSManager.swift
  5. 9 1
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  6. 67 0
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  7. 7 5
      Trio/Sources/Models/BloodGlucose.swift
  8. 0 32
      Trio/Sources/Models/Glucose.swift
  9. 19 6
      Trio/Sources/Modules/Main/MainStateModel.swift
  10. 1 7
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  11. 290 126
      Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift
  12. 63 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsContentsStepView.swift
  13. 62 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsImportantNotesStepView.swift
  14. 0 70
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsStepView.swift
  15. 52 65
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CompletedStepView.swift
  16. 10 50
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OverviewStepView.swift
  17. 53 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuide/StartupForceCloseWarningStepView.swift
  18. 45 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuide/StartupGuideStepView.swift
  19. 67 0
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuide/StartupReturningUserStepView.swift
  20. 0 71
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuideStepView.swift
  21. 22 18
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift
  22. 7 0
      Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift
  23. 119 11
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  24. 1 1
      Trio/Sources/Services/Bluetooth/BluetoothStateManager.swift
  25. 5 1
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  26. 82 0
      TrioTests/JSONImporterData/carbhistory.json
  27. 224 0
      TrioTests/JSONImporterData/enacted.json
  28. 3012 0
      TrioTests/JSONImporterData/glucose.json
  29. 173 0
      TrioTests/JSONImporterData/newerSuggested.json
  30. 557 0
      TrioTests/JSONImporterData/pumphistory-24h-zoned.json
  31. 223 0
      TrioTests/JSONImporterData/suggested.json
  32. 395 0
      TrioTests/JSONImporterTests.swift
  33. 41 0
      scripts/pump-history-stats.py

+ 12 - 0
Model/Helper/NSPredicates.swift

@@ -120,4 +120,16 @@ extension NSPredicate {
         let date = Date.threeMonthsAgo
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
+
+    static func predicateForDateBetween(start: Date, end: Date) -> NSPredicate {
+        NSPredicate(format: "date >= %@ AND date <= %@", start as NSDate, end as NSDate)
+    }
+
+    static func predicateForDeliverAtBetween(start: Date, end: Date) -> NSPredicate {
+        NSPredicate(format: "deliverAt >= %@ AND deliverAt <= %@", start as NSDate, end as NSDate)
+    }
+
+    static func predicateForTimestampBetween(start: Date, end: Date) -> NSPredicate {
+        NSPredicate(format: "timestamp >= %@ AND timestamp <= %@", start as NSDate, end as NSDate)
+    }
 }

+ 748 - 0
Model/JSONImporter.swift

@@ -0,0 +1,748 @@
+import CoreData
+import Foundation
+
+/// Migration-specific errors that might happen during migration
+enum JSONImporterError: Error {
+    case missingGlucoseValueInGlucoseEntry
+    case tempBasalAndDurationMismatch
+    case missingRequiredPropertyInPumpEntry
+    case suspendResumePumpEventMismatch
+    case duplicatePumpEvents
+    case missingCarbsValueInCarbEntry
+    case missingRequiredPropertyInDetermination(String)
+    case invalidDeterminationReason
+
+    var errorDescription: String? {
+        switch self {
+        case let .missingRequiredPropertyInDetermination(field):
+            return "Missing required property: \(field)"
+        case .invalidDeterminationReason:
+            return "Determination reason cannot be empty!"
+        default:
+            return nil
+        }
+    }
+}
+
+// MARK: - JSONImporter Class
+
+/// Responsible for importing JSON data into Core Data.
+///
+/// The importer handles two important states:
+/// - JSON files stored in the file system that contain data to import
+/// - Existing entries in CoreData that should not be duplicated
+///
+/// Imports are performed when a JSON file exists. The importer checks
+/// CoreData for existing entries to avoid duplicating records from partial imports.
+class JSONImporter {
+    private let context: NSManagedObjectContext
+    private let coreDataStack: CoreDataStack
+
+    /// Initializes the importer with a Core Data context.
+    init(context: NSManagedObjectContext, coreDataStack: CoreDataStack) {
+        self.context = context
+        self.coreDataStack = coreDataStack
+    }
+
+    /// Reads and parses a JSON file from the file system.
+    ///
+    /// - Parameters:
+    ///   - url: The URL of the JSON file to read.
+    /// - Returns: A decoded object of the specified type.
+    /// - Throws: An error if the file cannot be read or decoded.
+    private func readJsonFile<T: Decodable>(url: URL) throws -> T {
+        let data = try Data(contentsOf: url)
+        let decoder = JSONCoding.decoder
+        return try decoder.decode(T.self, from: data)
+    }
+
+    /// Retrieves the set of dates for all glucose values currently stored in CoreData.
+    ///
+    /// - Parameters: the start and end dates to fetch glucose values, inclusive
+    /// - Returns: A set of dates corresponding to existing glucose readings.
+    /// - Throws: An error if the fetch operation fails.
+    private func fetchGlucoseDates(start: Date, end: Date) async throws -> Set<Date> {
+        let allReadings = try await coreDataStack.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: .predicateForDateBetween(start: start, end: end),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored] ?? []
+
+        return Set(allReadings.compactMap(\.date))
+    }
+
+    /// Retrieves the set of timestamps for all pump events currently stored in CoreData.
+    ///
+    /// - Parameters: the start and end dates to fetch pump events, inclusive
+    /// - Returns: A set of dates corresponding to existing pump events.
+    /// - Throws: An error if the fetch operation fails.
+    private func fetchPumpTimestamps(start: Date, end: Date) async throws -> Set<Date> {
+        let allPumpEvents = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: .predicateForTimestampBetween(start: start, end: end),
+            key: "timestamp",
+            ascending: false
+        ) as? [PumpEventStored] ?? []
+
+        return Set(allPumpEvents.compactMap(\.timestamp))
+    }
+
+    /// Retrieves the set of timestamps for all carb entries currently stored in CoreData.
+    ///
+    /// - Parameters: the start and end dates to fetch carb entries, inclusive
+    /// - Returns: A set of dates corresponding to existing carb entries.
+    /// - Throws: An error if the fetch operation fails.
+    private func fetchCarbEntryDates(start: Date, end: Date) async throws -> Set<Date> {
+        let allCarbEntryDates = try await coreDataStack.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: .predicateForDateBetween(start: start, end: end),
+            key: "date",
+            ascending: false
+        ) as? [CarbEntryStored] ?? []
+
+        return Set(allCarbEntryDates.compactMap(\.date))
+    }
+
+    /// Retrieves the set of dates for all oref determinations currently stored in CoreData.
+    ///
+    /// - Parameters:
+    ///   - start: the start to fetch from; inclusive
+    ///   - end: the end date to fetch to; inclusive
+    /// - Returns: A set of dates corresponding to existing determinations.
+    /// - Throws: An error if the fetch operation fails.
+    private func fetchDeterminationDates(start: Date, end: Date) async throws -> Set<Date> {
+        let determinations = try await coreDataStack.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: .predicateForDeliverAtBetween(start: start, end: end),
+            key: "deliverAt",
+            ascending: false
+        ) as? [OrefDetermination] ?? []
+
+        return Set(determinations.compactMap(\.deliverAt))
+    }
+
+    /// Imports glucose history from a JSON file into CoreData.
+    ///
+    /// The function reads glucose data from the provided JSON file and stores new entries
+    /// in CoreData, skipping entries with dates that already exist in the database.
+    ///
+    /// - Parameters:
+    ///   - url: The URL of the JSON file containing glucose history.
+    ///   - now: The current time, used to skip old entries
+    /// - Throws:
+    ///   - JSONImporterError.missingGlucoseValueInGlucoseEntry if a glucose entry is missing a value.
+    ///   - An error if the file cannot be read or decoded.
+    ///   - An error if the CoreData operation fails.
+    func importGlucoseHistory(url: URL, now: Date) async throws {
+        let twentyFourHoursAgo = now - 24.hours.timeInterval
+        let glucoseHistoryFull: [BloodGlucose] = try readJsonFile(url: url)
+        let existingDates = try await fetchGlucoseDates(start: twentyFourHoursAgo, end: now)
+
+        // only import glucose values from the last 24 hours that don't exist
+        let glucoseHistory = glucoseHistoryFull
+            .filter { $0.dateString >= twentyFourHoursAgo && $0.dateString <= now && !existingDates.contains($0.dateString) }
+
+        // Create a background context for batch processing
+        let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
+        backgroundContext.parent = context
+
+        try await backgroundContext.perform {
+            for glucoseEntry in glucoseHistory {
+                try glucoseEntry.store(in: backgroundContext)
+            }
+
+            try backgroundContext.save()
+        }
+
+        try await context.perform {
+            try self.context.save()
+        }
+    }
+
+    /// combines tempBasal and tempBasalDuration events into one PumpHistoryEvent
+    private func combineTempBasalAndDuration(pumpHistory: [PumpHistoryEvent]) throws -> [PumpHistoryEvent] {
+        let tempBasal = pumpHistory.filter({ $0.type == .tempBasal }).sorted { $0.timestamp < $1.timestamp }
+        let tempBasalDuration = pumpHistory.filter({ $0.type == .tempBasalDuration }).sorted { $0.timestamp < $1.timestamp }
+        let nonTempBasal = pumpHistory.filter { $0.type != .tempBasal && $0.type != .tempBasalDuration }
+
+        guard tempBasal.count == tempBasalDuration.count else {
+            throw JSONImporterError.tempBasalAndDurationMismatch
+        }
+
+        let combinedTempBasal = try zip(tempBasal, tempBasalDuration).map { rate, duration in
+            guard rate.timestamp == duration.timestamp else {
+                throw JSONImporterError.tempBasalAndDurationMismatch
+            }
+            return PumpHistoryEvent(
+                id: duration.id,
+                type: .tempBasal,
+                timestamp: duration.timestamp,
+                duration: duration.durationMin,
+                rate: rate.rate,
+                temp: rate.temp
+            )
+        }
+
+        return (combinedTempBasal + nonTempBasal).sorted { $0.timestamp < $1.timestamp }
+    }
+
+    /// checks for pumpHistory inconsistencies that might cause issues if we import these events into CoreData
+    private func checkForInconsistencies(pumpHistory: [PumpHistoryEvent]) throws {
+        // make sure that pump suspends / resumes match up
+        let suspendsAndResumes = pumpHistory.filter({ $0.type == .pumpSuspend || $0.type == .pumpResume })
+            .sorted { $0.timestamp < $1.timestamp }
+
+        for (current, next) in zip(suspendsAndResumes, suspendsAndResumes.dropFirst()) {
+            guard current.type != next.type else {
+                throw JSONImporterError.suspendResumePumpEventMismatch
+            }
+        }
+
+        // check for duplicate events
+        struct TypeTimestamp: Hashable {
+            let timestamp: Date
+            let type: EventType
+        }
+
+        let duplicates = Dictionary(grouping: pumpHistory) { TypeTimestamp(timestamp: $0.timestamp, type: $0.type) }
+            .values.first(where: { $0.count > 1 })
+
+        if duplicates != nil {
+            throw JSONImporterError.duplicatePumpEvents
+        }
+    }
+
+    /// Imports pump history from a JSON file into CoreData.
+    ///
+    /// The function reads pump history data from the provided JSON file and stores new entries
+    /// in CoreData, skipping entries with timestamps that already exist in the database.
+    ///
+    /// - Parameters:
+    ///   - url: The URL of the JSON file containing pump history.
+    ///   - now: The current time, used to skip old entries
+    /// - Throws:
+    ///   - JSONImporterError.tempBasalAndDurationMismatch if we can't match tempBasals with their duration.
+    ///   - An error if the file cannot be read or decoded.
+    ///   - An error if the CoreData operation fails.
+    func importPumpHistory(url: URL, now: Date) async throws {
+        let twentyFourHoursAgo = now - 24.hours.timeInterval
+        let pumpHistoryRaw: [PumpHistoryEvent] = try readJsonFile(url: url)
+        let existingTimestamps = try await fetchPumpTimestamps(start: twentyFourHoursAgo, end: now)
+        let pumpHistoryFiltered = pumpHistoryRaw
+            .filter { $0.timestamp >= twentyFourHoursAgo && $0.timestamp <= now && !existingTimestamps.contains($0.timestamp) }
+
+        let pumpHistory = try combineTempBasalAndDuration(pumpHistory: pumpHistoryFiltered)
+        try checkForInconsistencies(pumpHistory: pumpHistory)
+
+        // Create a background context for batch processing
+        let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
+        backgroundContext.parent = context
+
+        try await backgroundContext.perform {
+            for pumpEntry in pumpHistory {
+                try pumpEntry.store(in: backgroundContext)
+            }
+
+            try backgroundContext.save()
+        }
+
+        try await context.perform {
+            try self.context.save()
+        }
+    }
+
+    /// Imports carb history from a JSON file into CoreData.
+    ///
+    /// The function reads carb entries data from the provided JSON file and stores new entries
+    /// in CoreData, skipping entries with dates that already exist in the database.
+    /// We ignore all FPU entries (aka carb equivalents) when performing an import.
+    ///
+    /// - Parameters:
+    ///   - url: The URL of the JSON file containing glucose history.
+    ///   - now: The current datetime
+    /// - Throws:
+    ///   - JSONImporterError.missingCarbsValueInCarbEntry if a carb entry is missing a `carbs: Decimal` value.
+    ///   - An error if the file cannot be read or decoded.
+    ///   - An error if the CoreData operation fails.
+    func importCarbHistory(url: URL, now: Date) async throws {
+        let twentyFourHoursAgo = now - 24.hours.timeInterval
+        let carbHistoryFull: [CarbsEntry] = try readJsonFile(url: url)
+        let existingDates = try await fetchCarbEntryDates(start: twentyFourHoursAgo, end: now)
+
+        // Only import carb entries from the last 24 hours that do not exist yet in Core Data
+        // Only import "true" carb entries; ignore all FPU entries (aka carb equivalents)
+        let carbHistory = carbHistoryFull
+            .filter {
+                let dateToCheck = $0.actualDate ?? $0.createdAt
+                return dateToCheck >= twentyFourHoursAgo && dateToCheck <= now && !existingDates.contains(dateToCheck) && $0
+                    .isFPU ?? false == false }
+
+        // Create a background context for batch processing
+        let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
+        backgroundContext.parent = context
+
+        try await backgroundContext.perform {
+            for carbEntry in carbHistory {
+                try carbEntry.store(in: backgroundContext)
+            }
+
+            try backgroundContext.save()
+        }
+
+        try await context.perform {
+            try self.context.save()
+        }
+    }
+
+    /// Imports oref determination from a JSON file into CoreData.
+    ///
+    /// The function reads oref determination data from the provided JSON file and stores new entries
+    /// in CoreData, skipping entries with dates that already exist in the database.
+    ///
+    /// - Parameters:
+    ///   - url: The URL of the JSON file containing determination data.
+    /// - Throws:
+    ///   - JSONImporterError.missingGlucoseValueInGlucoseEntry if a glucose entry is missing a value.
+    ///   - An error if the file cannot be read or decoded.
+    ///   - An error if the CoreData operation fails.
+    func importOrefDetermination(enactedUrl: URL, suggestedUrl: URL, now: Date) async throws {
+        let twentyFourHoursAgo = now - 24.hours.timeInterval
+        let enactedDetermination: Determination = try readJsonFile(url: enactedUrl)
+        let suggestedDetermination: Determination = try readJsonFile(url: suggestedUrl)
+        let existingDates = try await fetchDeterminationDates(start: twentyFourHoursAgo, end: now)
+
+        /// Helper function to check if entries are from within the last 24 hours that do not yet exist in Core Data
+        func checkDeterminationDate(_ date: Date) -> Bool {
+            date >= twentyFourHoursAgo && date <= now && !existingDates.contains(date)
+        }
+
+        guard let enactedDeliverAt = enactedDetermination.deliverAt,
+              let suggestedDeliverAt = suggestedDetermination.deliverAt
+        else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("deliverAt")
+        }
+
+        guard checkDeterminationDate(enactedDeliverAt), checkDeterminationDate(suggestedDeliverAt) else {
+            return
+        }
+
+        try enactedDetermination.checkForRequiredFields()
+        try suggestedDetermination.checkForRequiredFields()
+
+        // Create a background context for batch processing
+        let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
+        backgroundContext.parent = context
+
+        try await backgroundContext.perform {
+            /// We know both determination entries are from within last 24 hrs via `checkDeterminationDate()` in the earlier `guard` clause
+            /// If their `deliverAt` does not match, and if `suggestedDeliverAt` is newer, it is worth storing them both, as that represents
+            /// a more recent algorithm run that did not cause a dosing enactment, e.g., a carb entry or a manual bolus.
+            if suggestedDeliverAt > enactedDeliverAt {
+                try suggestedDetermination.store(in: backgroundContext)
+            }
+
+            try enactedDetermination.store(in: backgroundContext)
+
+            try backgroundContext.save()
+        }
+
+        try await context.perform {
+            try self.context.save()
+        }
+    }
+}
+
+// MARK: - Extension for Specific Import Functions
+
+extension BloodGlucose {
+    /// Helper function to convert `BloodGlucose` to `GlucoseStored` while importing JSON glucose entries
+    func store(in context: NSManagedObjectContext) throws {
+        guard let glucoseValue = glucose ?? sgv else {
+            throw JSONImporterError.missingGlucoseValueInGlucoseEntry
+        }
+
+        let glucoseEntry = GlucoseStored(context: context)
+        glucoseEntry.id = _id.flatMap({ UUID(uuidString: $0) }) ?? UUID()
+        glucoseEntry.date = dateString
+        glucoseEntry.glucose = Int16(glucoseValue)
+        glucoseEntry.direction = direction?.rawValue
+        glucoseEntry.isManual = type == "Manual"
+        glucoseEntry.isUploadedToNS = true
+        glucoseEntry.isUploadedToHealth = true
+        glucoseEntry.isUploadedToTidepool = true
+    }
+}
+
+extension PumpHistoryEvent {
+    /// Helper function to convert `PumpHistoryEvent` to `PumpEventStored` while importing JSON pump histories
+    func store(in context: NSManagedObjectContext) throws {
+        let pumpEntry = PumpEventStored(context: context)
+        pumpEntry.id = id
+        pumpEntry.timestamp = timestamp
+        pumpEntry.type = type.rawValue
+        pumpEntry.isUploadedToNS = true
+        pumpEntry.isUploadedToHealth = true
+        pumpEntry.isUploadedToTidepool = true
+
+        if type == .bolus {
+            guard let amount = amount else {
+                throw JSONImporterError.missingRequiredPropertyInPumpEntry
+            }
+            let bolusEntry = BolusStored(context: context)
+            bolusEntry.amount = NSDecimalNumber(decimal: amount)
+            bolusEntry.isSMB = isSMB ?? false
+            bolusEntry.isExternal = isExternal ?? false
+            pumpEntry.bolus = bolusEntry
+        } else if type == .tempBasal {
+            guard let rate = rate, let duration = duration else {
+                throw JSONImporterError.missingRequiredPropertyInPumpEntry
+            }
+            let tempEntry = TempBasalStored(context: context)
+            tempEntry.rate = NSDecimalNumber(decimal: rate)
+            tempEntry.duration = Int16(duration)
+            tempEntry.tempType = temp?.rawValue
+            pumpEntry.tempBasal = tempEntry
+        }
+    }
+}
+
+/// Extension to support decoding `CarbsEntry` from JSON with multiple possible key formats for entry notes.
+///
+/// This is needed because some JSON sources (e.g., Trio v0.2.5) use the singular key `"note"`
+/// for the `note` field, while others (e.g., Nightscout or oref) use the plural `"notes"`.
+///
+/// To ensure compatibility across all sources without duplicating models or requiring upstream fixes,
+/// this custom implementation attempts to decode the `note` field first from `"note"`, then from `"notes"`.
+/// Encoding will always use the canonical `"notes"` key to preserve consistency in output,
+/// as this is what's established throughout the backend now.
+extension CarbsEntry: Codable {
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        id = try container.decodeIfPresent(String.self, forKey: .id)
+        createdAt = try container.decode(Date.self, forKey: .createdAt)
+        actualDate = try container.decodeIfPresent(Date.self, forKey: .actualDate)
+        carbs = try container.decode(Decimal.self, forKey: .carbs)
+        fat = try container.decodeIfPresent(Decimal.self, forKey: .fat)
+        protein = try container.decodeIfPresent(Decimal.self, forKey: .protein)
+
+        // Handle both `note` and `notes`
+        if let noteValue = try? container.decodeIfPresent(String.self, forKey: .note) {
+            note = noteValue
+        } else if let notesValue = try? container.decodeIfPresent(String.self, forKey: .noteAlt) {
+            note = notesValue
+        } else {
+            note = nil
+        }
+
+        enteredBy = try container.decodeIfPresent(String.self, forKey: .enteredBy)
+        isFPU = try container.decodeIfPresent(Bool.self, forKey: .isFPU)
+        fpuID = try container.decodeIfPresent(String.self, forKey: .fpuID)
+    }
+
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+
+        try container.encodeIfPresent(id, forKey: .id)
+        try container.encode(createdAt, forKey: .createdAt)
+        try container.encodeIfPresent(actualDate, forKey: .actualDate)
+        try container.encode(carbs, forKey: .carbs)
+        try container.encodeIfPresent(fat, forKey: .fat)
+        try container.encodeIfPresent(protein, forKey: .protein)
+        try container.encodeIfPresent(note, forKey: .note)
+        try container.encodeIfPresent(enteredBy, forKey: .enteredBy)
+        try container.encodeIfPresent(isFPU, forKey: .isFPU)
+        try container.encodeIfPresent(fpuID, forKey: .fpuID)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case id = "_id"
+        case createdAt = "created_at"
+        case actualDate
+        case carbs
+        case fat
+        case protein
+        case note = "notes" // standard key
+        case noteAlt = "note" // import key
+        case enteredBy
+        case isFPU
+        case fpuID
+    }
+
+    /// Helper function to convert `CarbsStored` to `CarbEntryStored` while importing JSON carb entries
+    func store(in context: NSManagedObjectContext) throws {
+        guard carbs >= 0 else {
+            throw JSONImporterError.missingCarbsValueInCarbEntry
+        }
+
+        // skip FPU entries for now
+
+        let carbEntry = CarbEntryStored(context: context)
+        carbEntry.id = id
+            .flatMap({ UUID(uuidString: $0) }) ?? UUID() /// The `CodingKey` of `id` is `_id`, so this fine to use here
+        carbEntry.date = actualDate ?? createdAt
+        carbEntry.carbs = Double(truncating: NSDecimalNumber(decimal: carbs))
+        carbEntry.fat = Double(truncating: NSDecimalNumber(decimal: fat ?? 0))
+        carbEntry.protein = Double(truncating: NSDecimalNumber(decimal: protein ?? 0))
+        carbEntry.note = note ?? ""
+        carbEntry.isFPU = false
+        carbEntry.isUploadedToNS = true
+        carbEntry.isUploadedToHealth = true
+        carbEntry.isUploadedToTidepool = true
+
+        if fat != nil, protein != nil, let fpuId = fpuID {
+            carbEntry.fpuID = UUID(uuidString: fpuId)
+        }
+    }
+}
+
+/// Extension to support decoding `Determination` entries with misspelled keys from external JSON sources.
+///
+/// Some legacy or third-party tools occasionally serialize the `received` property as `"recieved"`
+/// (misspelled) instead of the correct `"received"`. To prevent decoding failures or data loss,
+/// this custom decoder attempts to decode from `"received"` first, then falls back to `"recieved"`
+/// if necessary.
+///
+/// Encoding always uses the correct `"received"` key to ensure consistent, standards-compliant output.
+///
+/// This improves resilience and ensures compatibility with imported loop history, simulations,
+/// or devicestatus artifacts that may contain typos in their keys.
+extension Determination: Codable {
+    private enum CodingKeys: String, CodingKey {
+        case id
+        case reason
+        case units
+        case insulinReq
+        case eventualBG
+        case sensitivityRatio
+        case rate
+        case duration
+        case iob = "IOB"
+        case cob = "COB"
+        case predictions = "predBGs"
+        case deliverAt
+        case carbsReq
+        case temp
+        case bg
+        case reservoir
+        case timestamp
+        case isf = "ISF"
+        case current_target
+        case tdd = "TDD"
+        case insulinForManualBolus
+        case manualBolusErrorString
+        case minDelta
+        case expectedDelta
+        case minGuardBG
+        case minPredBG
+        case threshold
+        case carbRatio = "CR"
+        case received
+        case receivedAlt = "recieved"
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        id = try container.decodeIfPresent(UUID.self, forKey: .id)
+        reason = try container.decode(String.self, forKey: .reason)
+        units = try container.decodeIfPresent(Decimal.self, forKey: .units)
+        insulinReq = try container.decodeIfPresent(Decimal.self, forKey: .insulinReq)
+        eventualBG = try container.decodeIfPresent(Int.self, forKey: .eventualBG)
+        sensitivityRatio = try container.decodeIfPresent(Decimal.self, forKey: .sensitivityRatio)
+        rate = try container.decodeIfPresent(Decimal.self, forKey: .rate)
+        duration = try container.decodeIfPresent(Decimal.self, forKey: .duration)
+        iob = try container.decodeIfPresent(Decimal.self, forKey: .iob)
+        cob = try container.decodeIfPresent(Decimal.self, forKey: .cob)
+        predictions = try container.decodeIfPresent(Predictions.self, forKey: .predictions)
+        deliverAt = try container.decodeIfPresent(Date.self, forKey: .deliverAt)
+        carbsReq = try container.decodeIfPresent(Decimal.self, forKey: .carbsReq)
+        temp = try container.decodeIfPresent(TempType.self, forKey: .temp)
+        bg = try container.decodeIfPresent(Decimal.self, forKey: .bg)
+        reservoir = try container.decodeIfPresent(Decimal.self, forKey: .reservoir)
+        timestamp = try container.decodeIfPresent(Date.self, forKey: .timestamp)
+        isf = try container.decodeIfPresent(Decimal.self, forKey: .isf)
+        current_target = try container.decodeIfPresent(Decimal.self, forKey: .current_target)
+        tdd = try container.decodeIfPresent(Decimal.self, forKey: .tdd)
+        insulinForManualBolus = try container.decodeIfPresent(Decimal.self, forKey: .insulinForManualBolus)
+        manualBolusErrorString = try container.decodeIfPresent(Decimal.self, forKey: .manualBolusErrorString)
+        minDelta = try container.decodeIfPresent(Decimal.self, forKey: .minDelta)
+        expectedDelta = try container.decodeIfPresent(Decimal.self, forKey: .expectedDelta)
+        minGuardBG = try container.decodeIfPresent(Decimal.self, forKey: .minGuardBG)
+        minPredBG = try container.decodeIfPresent(Decimal.self, forKey: .minPredBG)
+        threshold = try container.decodeIfPresent(Decimal.self, forKey: .threshold)
+        carbRatio = try container.decodeIfPresent(Decimal.self, forKey: .carbRatio)
+
+        // Handle both spellings of "received"
+        if let value = try container.decodeIfPresent(Bool.self, forKey: .received) {
+            received = value
+        } else if let fallback = try container.decodeIfPresent(Bool.self, forKey: .receivedAlt) {
+            received = fallback
+        } else {
+            received = nil
+        }
+    }
+
+    func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+
+        try container.encodeIfPresent(id, forKey: .id)
+        try container.encode(reason, forKey: .reason)
+        try container.encodeIfPresent(units, forKey: .units)
+        try container.encodeIfPresent(insulinReq, forKey: .insulinReq)
+        try container.encodeIfPresent(eventualBG, forKey: .eventualBG)
+        try container.encodeIfPresent(sensitivityRatio, forKey: .sensitivityRatio)
+        try container.encodeIfPresent(rate, forKey: .rate)
+        try container.encodeIfPresent(duration, forKey: .duration)
+        try container.encodeIfPresent(iob, forKey: .iob)
+        try container.encodeIfPresent(cob, forKey: .cob)
+        try container.encodeIfPresent(predictions, forKey: .predictions)
+        try container.encodeIfPresent(deliverAt, forKey: .deliverAt)
+        try container.encodeIfPresent(carbsReq, forKey: .carbsReq)
+        try container.encodeIfPresent(temp, forKey: .temp)
+        try container.encodeIfPresent(bg, forKey: .bg)
+        try container.encodeIfPresent(reservoir, forKey: .reservoir)
+        try container.encodeIfPresent(timestamp, forKey: .timestamp)
+        try container.encodeIfPresent(isf, forKey: .isf)
+        try container.encodeIfPresent(current_target, forKey: .current_target)
+        try container.encodeIfPresent(tdd, forKey: .tdd)
+        try container.encodeIfPresent(insulinForManualBolus, forKey: .insulinForManualBolus)
+        try container.encodeIfPresent(manualBolusErrorString, forKey: .manualBolusErrorString)
+        try container.encodeIfPresent(minDelta, forKey: .minDelta)
+        try container.encodeIfPresent(expectedDelta, forKey: .expectedDelta)
+        try container.encodeIfPresent(minGuardBG, forKey: .minGuardBG)
+        try container.encodeIfPresent(minPredBG, forKey: .minPredBG)
+        try container.encodeIfPresent(threshold, forKey: .threshold)
+        try container.encodeIfPresent(carbRatio, forKey: .carbRatio)
+        try container.encodeIfPresent(received, forKey: .received) // always encode the correct spelling
+    }
+
+    func checkForRequiredFields() throws {
+        guard let deliverAt = deliverAt else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("deliverAt")
+        }
+        guard let timestamp = timestamp else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("timestamp")
+        }
+        guard reason.isNotEmpty else {
+            throw JSONImporterError.invalidDeterminationReason
+        }
+        guard let insulinReq = insulinReq else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("insulinReq")
+        }
+        guard let currentTarget = current_target else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("current_target")
+        }
+        guard let reservoir = reservoir else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("reservoir")
+        }
+        guard let threshold = threshold else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("threshold")
+        }
+        guard let iob = iob else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("IOB")
+        }
+        guard let isf = isf else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("ISF")
+        }
+        guard let manualBolusErrorString = manualBolusErrorString else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("manualBolusErrorString")
+        }
+        guard let insulinForManualBolus = insulinForManualBolus else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("insulinForManualBolus")
+        }
+        guard let cob = cob else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("COB")
+        }
+        guard let tdd = tdd else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("TDD")
+        }
+        guard let bg = bg else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("bg")
+        }
+        guard let minDelta = minDelta else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("minDelta")
+        }
+        guard let eventualBG = eventualBG else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("eventualBG")
+        }
+        guard let sensitivityRatio = sensitivityRatio else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("sensitivityRatio")
+        }
+        guard let temp = temp else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("temp")
+        }
+        guard let expectedDelta = expectedDelta else {
+            throw JSONImporterError.missingRequiredPropertyInDetermination("expectedDelta")
+        }
+    }
+
+    /// Helper function to convert `Determination` to `OrefDetermination` while importing JSON glucose entries
+    func store(in context: NSManagedObjectContext) throws {
+        let newOrefDetermination = OrefDetermination(context: context)
+        newOrefDetermination.id = UUID()
+        newOrefDetermination.insulinSensitivity = decimalToNSDecimalNumber(isf)
+        newOrefDetermination.currentTarget = decimalToNSDecimalNumber(current_target)
+        newOrefDetermination.eventualBG = eventualBG.map(NSDecimalNumber.init)
+        newOrefDetermination.deliverAt = deliverAt
+        newOrefDetermination.timestamp = timestamp
+        newOrefDetermination.enacted = received ?? false
+        newOrefDetermination.insulinForManualBolus = decimalToNSDecimalNumber(insulinForManualBolus)
+        newOrefDetermination.carbRatio = decimalToNSDecimalNumber(carbRatio)
+        newOrefDetermination.glucose = decimalToNSDecimalNumber(bg)
+        newOrefDetermination.reservoir = decimalToNSDecimalNumber(reservoir)
+        newOrefDetermination.insulinReq = decimalToNSDecimalNumber(insulinReq)
+        newOrefDetermination.temp = temp?.rawValue ?? "absolute"
+        newOrefDetermination.rate = decimalToNSDecimalNumber(rate)
+        newOrefDetermination.reason = reason
+        newOrefDetermination.duration = decimalToNSDecimalNumber(duration)
+        newOrefDetermination.iob = decimalToNSDecimalNumber(iob)
+        newOrefDetermination.threshold = decimalToNSDecimalNumber(threshold)
+        newOrefDetermination.minDelta = decimalToNSDecimalNumber(minDelta)
+        newOrefDetermination.sensitivityRatio = decimalToNSDecimalNumber(sensitivityRatio)
+        newOrefDetermination.expectedDelta = decimalToNSDecimalNumber(expectedDelta)
+        newOrefDetermination.cob = Int16(Int(cob ?? 0))
+        newOrefDetermination.manualBolusErrorString = decimalToNSDecimalNumber(manualBolusErrorString)
+        newOrefDetermination.smbToDeliver = units.map { NSDecimalNumber(decimal: $0) }
+        newOrefDetermination.carbsRequired = Int16(Int(carbsReq ?? 0))
+        newOrefDetermination.isUploadedToNS = true
+
+        if let predictions = predictions {
+            ["iob": predictions.iob, "zt": predictions.zt, "cob": predictions.cob, "uam": predictions.uam]
+                .forEach { type, values in
+                    if let values = values {
+                        let forecast = Forecast(context: context)
+                        forecast.id = UUID()
+                        forecast.type = type
+                        forecast.date = Date()
+                        forecast.orefDetermination = newOrefDetermination
+
+                        for (index, value) in values.enumerated() {
+                            let forecastValue = ForecastValue(context: context)
+                            forecastValue.index = Int32(index)
+                            forecastValue.value = Int32(value)
+                            forecast.addToForecastValues(forecastValue)
+                        }
+                        newOrefDetermination.addToForecasts(forecast)
+                    }
+                }
+        }
+    }
+
+    func decimalToNSDecimalNumber(_ value: Decimal?) -> NSDecimalNumber? {
+        guard let value = value else { return nil }
+        return NSDecimalNumber(decimal: value)
+    }
+}
+
+extension JSONImporter {
+    func importGlucoseHistoryIfNeeded() async {}
+    func importPumpHistoryIfNeeded() async {}
+    func importCarbHistoryIfNeeded() async {}
+    func importDeterminationIfNeeded() async {}
+}

+ 66 - 10
Trio.xcodeproj/project.pbxproj

@@ -104,7 +104,6 @@
 		383420D625FFE38C002D46C1 /* LoopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D525FFE38C002D46C1 /* LoopView.swift */; };
 		383420D925FFEB3F002D46C1 /* Popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D825FFEB3F002D46C1 /* Popup.swift */; };
 		383948D625CD4D8900E91849 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D525CD4D8900E91849 /* FileStorage.swift */; };
-		383948DA25CD64D500E91849 /* Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D925CD64D500E91849 /* Glucose.swift */; };
 		384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803325C385E60086DB71 /* JavaScriptWorker.swift */; };
 		384E803825C388640086DB71 /* Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803725C388640086DB71 /* Script.swift */; };
 		38569347270B5DFB0002C50D /* CGMType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38569344270B5DFA0002C50D /* CGMType.swift */; };
@@ -245,8 +244,12 @@
 		3B4BA78F2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; };
 		3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
+		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
+		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
+		3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */; };
 		3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; };
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
@@ -553,6 +556,7 @@
 		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */; };
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
+		DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DD3C47B22DC5608A003DD20D /* newerSuggested.json */; };
 		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
 		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
 		DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */; };
@@ -564,7 +568,7 @@
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD4A00212DAEEED800AB7387 /* OnboardingView+AlgorithmUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */; };
 		DD4A00242DAEF5E400AB7387 /* AlgorithmSettingsSubstepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */; };
-		DD4AFFF12DADB59100AB7387 /* AlgorithmSettingsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsStepView.swift */; };
+		DD4AFFF12DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */; };
 		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
 		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */; };
 		DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */; };
@@ -576,6 +580,9 @@
 		DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6A4E082DBD95F1008C4B26 /* BluetoothRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A4E072DBD95F1008C4B26 /* BluetoothRequiredView.swift */; };
+		DD6A4E7E2DBEBF0F008C4B26 /* StartupReturningUserStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A4E7D2DBEBF0F008C4B26 /* StartupReturningUserStepView.swift */; };
+		DD6A4E802DBEC3EE008C4B26 /* StartupForceCloseWarningStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A4E7F2DBEC3EE008C4B26 /* StartupForceCloseWarningStepView.swift */; };
+		DD6A4E842DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A4E832DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
@@ -617,6 +624,9 @@
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
 		DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */; };
+		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
+		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
+		DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD82DC421B500AC63F3 /* suggested.json */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
@@ -920,7 +930,6 @@
 		383420D525FFE38C002D46C1 /* LoopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopView.swift; sourceTree = "<group>"; };
 		383420D825FFEB3F002D46C1 /* Popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Popup.swift; sourceTree = "<group>"; };
 		383948D525CD4D8900E91849 /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = "<group>"; };
-		383948D925CD64D500E91849 /* Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glucose.swift; sourceTree = "<group>"; };
 		384E803325C385E60086DB71 /* JavaScriptWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptWorker.swift; sourceTree = "<group>"; };
 		384E803725C388640086DB71 /* Script.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Script.swift; sourceTree = "<group>"; };
 		38569344270B5DFA0002C50D /* CGMType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMType.swift; sourceTree = "<group>"; };
@@ -1042,8 +1051,12 @@
 		3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
+		3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporterTests.swift; sourceTree = "<group>"; };
+		3B997DD12DC02AEF006B6BB2 /* glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = glucose.json; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
+		3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-24h-zoned.json"; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1356,6 +1369,7 @@
 		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
+		DD3C47B22DC5608A003DD20D /* newerSuggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = newerSuggested.json; sourceTree = "<group>"; };
 		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
 		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
 		DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingEditorView.swift; sourceTree = "<group>"; };
@@ -1364,7 +1378,7 @@
 		DD3F1F8F2D9E153A00DCE7B3 /* NightscoutImportStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportStepView.swift; sourceTree = "<group>"; };
 		DD4A00202DAEEEC400AB7387 /* OnboardingView+AlgorithmUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AlgorithmUtil.swift"; sourceTree = "<group>"; };
 		DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsSubstepView.swift; sourceTree = "<group>"; };
-		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsStepView.swift; sourceTree = "<group>"; };
+		DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsContentsStepView.swift; sourceTree = "<group>"; };
 		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
 		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
 		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
@@ -1376,6 +1390,9 @@
 		DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Helpers.swift"; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6A4E072DBD95F1008C4B26 /* BluetoothRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothRequiredView.swift; sourceTree = "<group>"; };
+		DD6A4E7D2DBEBF0F008C4B26 /* StartupReturningUserStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupReturningUserStepView.swift; sourceTree = "<group>"; };
+		DD6A4E7F2DBEC3EE008C4B26 /* StartupForceCloseWarningStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupForceCloseWarningStepView.swift; sourceTree = "<group>"; };
+		DD6A4E832DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmSettingsImportantNotesStepView.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
@@ -1420,6 +1437,9 @@
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
 		DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedA1cDisplayUnit.swift; sourceTree = "<group>"; };
+		DDD78A902DC4064800AC63F3 /* carbhistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = carbhistory.json; sourceTree = "<group>"; };
+		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
+		DDD78AD82DC421B500AC63F3 /* suggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested.json; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -2318,7 +2338,6 @@
 				3811DF0125CA9FEA00A708ED /* Credentials.swift */,
 				E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */,
 				38AEE73C25F0200C0013F05B /* TrioSettings.swift */,
-				383948D925CD64D500E91849 /* Glucose.swift */,
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
 				38887CCD25F5725200944304 /* IOBEntry.swift */,
 				DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */,
@@ -2532,17 +2551,32 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			children = (
+				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
+				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
+				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
-				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
 				BD8FC0532D66186000B95AED /* TestError.swift */,
 			);
 			path = TrioTests;
 			sourceTree = "<group>";
 		};
+		3B997DD22DC02AEF006B6BB2 /* JSONImporterData */ = {
+			isa = PBXGroup;
+			children = (
+				DD3C47B22DC5608A003DD20D /* newerSuggested.json */,
+				DDD78AD72DC421B500AC63F3 /* enacted.json */,
+				DDD78AD82DC421B500AC63F3 /* suggested.json */,
+				DDD78A902DC4064800AC63F3 /* carbhistory.json */,
+				3B997DD12DC02AEF006B6BB2 /* glucose.json */,
+				3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */,
+			);
+			path = JSONImporterData;
+			sourceTree = "<group>";
+		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -2640,6 +2674,7 @@
 				3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
+				3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */,
 				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
 				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
 				5825D1622BD405AE00F36E9B /* Helper */,
@@ -2794,11 +2829,11 @@
 		BD47FDD52D8B64AE0043966B /* OnboardingSteps */ = {
 			isa = PBXGroup;
 			children = (
+				DD6A4E4E2DBEBC7B008C4B26 /* StartupGuide */,
 				DDFF20302DB1D15500AB8A96 /* BluetoothPermissionStepView.swift */,
 				DDFF202E2DB1D14500AB8A96 /* NotificationPermissionStepView.swift */,
 				DD4A00222DAEF5CD00AB7387 /* AlgorithmSettings */,
 				DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */,
-				DDF691362DA30332008BF16C /* StartupGuideStepView.swift */,
 				DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */,
 				DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */,
 				DD3F1F8E2D9E151200DCE7B3 /* Nightscout */,
@@ -3272,8 +3307,9 @@
 		DD4A00222DAEF5CD00AB7387 /* AlgorithmSettings */ = {
 			isa = PBXGroup;
 			children = (
+				DD6A4E832DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift */,
 				DD4A00232DAEF5DC00AB7387 /* AlgorithmSettingsSubstepView.swift */,
-				DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsStepView.swift */,
+				DD4AFFF02DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift */,
 			);
 			path = AlgorithmSettings;
 			sourceTree = "<group>";
@@ -3306,6 +3342,16 @@
 			path = ViewElements;
 			sourceTree = "<group>";
 		};
+		DD6A4E4E2DBEBC7B008C4B26 /* StartupGuide */ = {
+			isa = PBXGroup;
+			children = (
+				DD6A4E7F2DBEC3EE008C4B26 /* StartupForceCloseWarningStepView.swift */,
+				DD6A4E7D2DBEBF0F008C4B26 /* StartupReturningUserStepView.swift */,
+				DDF691362DA30332008BF16C /* StartupGuideStepView.swift */,
+			);
+			path = StartupGuide;
+			sourceTree = "<group>";
+		};
 		DD9ECB662CA99EFE00AA7C45 /* RemoteControl */ = {
 			isa = PBXGroup;
 			children = (
@@ -3875,6 +3921,12 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */,
+				DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */,
+				3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */,
+				DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */,
+				DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */,
+				DDD78ADA2DC421B500AC63F3 /* suggested.json in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -4046,7 +4098,6 @@
 				DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */,
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
-				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
 				CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */,
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */,
@@ -4129,6 +4180,7 @@
 				DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */,
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
+				DD6A4E802DBEC3EE008C4B26 /* StartupForceCloseWarningStepView.swift in Sources */,
 				DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */,
 				DDF691012DA2CA11008BF16C /* AppDiagnosticsDataFlow.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
@@ -4154,6 +4206,7 @@
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				E592A3702CEEC01E009A472C /* ContactTrickEntry.swift in Sources */,
+				DD6A4E7E2DBEBF0F008C4B26 /* StartupReturningUserStepView.swift in Sources */,
 				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
@@ -4255,6 +4308,7 @@
 				190EBCC429FF136900BA767D /* UserInterfaceSettingsDataFlow.swift in Sources */,
 				CE3EEF9A2D463717001944DD /* CustomCGMOptionsView.swift in Sources */,
 				5A2325582BFCC168003518CA /* NightscoutConnectView.swift in Sources */,
+				DD6A4E842DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
@@ -4390,7 +4444,7 @@
 				58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,
 				BD47FD172D88AAF50043966B /* CompletedStepView.swift in Sources */,
-				DD4AFFF12DADB59100AB7387 /* AlgorithmSettingsStepView.swift in Sources */,
+				DD4AFFF12DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift in Sources */,
 				DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */,
 				DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */,
 				BD47FDDB2D8B659B0043966B /* BasalProfileStepView.swift in Sources */,
@@ -4398,6 +4452,7 @@
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */,
+				3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */,
 				BD249D992D42FCCD00412DEB /* BolusStatsSetup.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
@@ -4561,6 +4616,7 @@
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
+				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,

+ 11 - 0
Trio/Sources/APS/APSManager.swift

@@ -49,6 +49,17 @@ enum APSError: LocalizedError {
             return String(localized: "Manual Temporary Basal Rate (\(message)). Looping suspended.")
         }
     }
+
+    static func pumpErrorMatches(message: String) -> Bool {
+        message.contains(String(localized: "Pump Error"))
+    }
+
+    static func pumpWarningMatches(message: String) -> Bool {
+        message.contains(String(localized: "Invalid Pump State")) || message
+            .contains("PumpMessage") || message
+            .contains("PumpOpsError") || message.contains("RileyLink") || message
+            .contains(String(localized: "Pump did not respond in time"))
+    }
 }
 
 final class BaseAPSManager: APSManager, Injectable {

+ 9 - 1
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -211,9 +211,17 @@ final class OpenAPS {
     }
 
     private func loadAndMapPumpEvents(_ pumpHistoryObjectIDs: [NSManagedObjectID]) -> [PumpEventDTO] {
+        OpenAPS.loadAndMapPumpEvents(pumpHistoryObjectIDs, from: context)
+    }
+
+    /// Fetches and parses pump events, expose this as static and not private for testing
+    static func loadAndMapPumpEvents(
+        _ pumpHistoryObjectIDs: [NSManagedObjectID],
+        from context: NSManagedObjectContext
+    ) -> [PumpEventDTO] {
         // Load the pump events from the object IDs
         let pumpHistory: [PumpEventStored] = pumpHistoryObjectIDs
-            .compactMap { self.context.object(with: $0) as? PumpEventStored }
+            .compactMap { context.object(with: $0) as? PumpEventStored }
 
         // Create the DTOs
         let dtos: [PumpEventDTO] = pumpHistory.flatMap { event -> [PumpEventDTO] in

+ 67 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -2687,6 +2687,9 @@
         }
       }
     },
+    " of usage data and is not yet configurable." : {
+
+    },
     " SMB" : {
       "comment" : "Super Micro Bolus indicator in delete alert",
       "localizations" : {
@@ -2897,6 +2900,7 @@
       }
     },
     " the app before finishing onboarding, " : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -19478,6 +19482,9 @@
         }
       }
     },
+    "7 days" : {
+
+    },
     "10-90" : {
       "localizations" : {
         "bg" : {
@@ -21795,6 +21802,7 @@
       }
     },
     "A few important notes:" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -21894,6 +21902,9 @@
         }
       }
     },
+    "A few important notes…" : {
+
+    },
     "A generated unique identifier (a random code like \"A7B2C9D3\" that doesn't identify you personally)" : {
       "localizations" : {
         "bg" : {
@@ -32207,6 +32218,9 @@
         }
       }
     },
+    "All entries you made during Onboarding will be saved automatically when you complete the wizard." : {
+
+    },
     "All FPUs and the carbs of the meal will be deleted." : {
       "comment" : "Alert message for meal deletion",
       "localizations" : {
@@ -84932,7 +84946,11 @@
         }
       }
     },
+    "Dynamic ISF requires at least " : {
+
+    },
     "Dynamic ISF requires at least 7 days of usage data and is not yet configurable." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -95116,7 +95134,11 @@
         }
       }
     },
+    "Even if you’re an updating user, you’ll be guided through the algorithm settings configuration step-by-step." : {
+
+    },
     "Even if you’re an updating user, you’ll be guided through this step-by-step." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -102939,6 +102961,7 @@
       }
     },
     "force quit" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -109742,6 +109765,7 @@
       }
     },
     "Got it! I'm ready to continue." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -111794,6 +111818,9 @@
         }
       }
     },
+    "Here's what you can expect to be preserved:" : {
+
+    },
     "Hi there!" : {
       "localizations" : {
         "bg" : {
@@ -119638,6 +119665,9 @@
         }
       }
     },
+    "Important" : {
+
+    },
     "Important message" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -124043,6 +124073,9 @@
         }
       }
     },
+    "Invalid Pump State" : {
+
+    },
     "Invalid Pump State (%@)." : {
       "localizations" : {
         "bg" : {
@@ -125969,6 +126002,9 @@
         }
       }
     },
+    "Just be aware: if you force quit the app before finishing onboarding, your progress will not be saved." : {
+
+    },
     "Keep these turned ON in your phone’s settings to ensure you receive Trio Notifications, Critical Alerts, and Time Sensitive Notifications." : {
       "localizations" : {
         "bg" : {
@@ -167225,6 +167261,12 @@
         }
       }
     },
+    "Pump did not respond in time" : {
+
+    },
+    "Pump Error" : {
+
+    },
     "Pump Error (%@)." : {
       "localizations" : {
         "bg" : {
@@ -189851,6 +189893,9 @@
         }
       }
     },
+    "Some helpful reminders:" : {
+
+    },
     "Some other cloud" : {
       "localizations" : {
         "bg" : {
@@ -238557,7 +238602,11 @@
         }
       }
     },
+    "You can pause at any time. If you feel like taking a break, do it and put the phone down!" : {
+
+    },
     "You can pause at any time. Just be aware: if you " : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -238857,6 +238906,9 @@
         }
       }
     },
+    "You will also be guided through re-configuring your algorithm settings, respecting Trio's new guardrails." : {
+
+    },
     "You're All Set!" : {
       "localizations" : {
         "bg" : {
@@ -239057,7 +239109,11 @@
         }
       }
     },
+    "Your algorithm settings (previously called \"OpenAPS settings\") are reset to defaults." : {
+
+    },
     "Your algorithm settings (previously called \"OpenAPS settings\") will be reset to defaults." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -239981,6 +240037,7 @@
       }
     },
     "your progress will not be saved." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -240080,6 +240137,9 @@
         }
       }
     },
+    "Your pump and CGM configurations are retained and fully functional." : {
+
+    },
     "Your Rights" : {
       "localizations" : {
         "bg" : {
@@ -240180,7 +240240,11 @@
         }
       }
     },
+    "Your therapy settings (basal rates, carb ratios, insulin sensitivities and glucose targets) are carried over." : {
+
+    },
     "Your therapy settings, pump, and CGM configurations will be carried over." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -240280,6 +240344,9 @@
         }
       }
     },
+    "Your treatment data (pump events, carb entries, glucose trace, etc.) are not migrated." : {
+
+    },
     "ZT" : {
       "localizations" : {
         "bg" : {

+ 7 - 5
Trio/Sources/Models/BloodGlucose.swift

@@ -79,11 +79,13 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         _id = try container.decode(String.self, forKey: ._id)
 
-        do {
-            sgv = try container.decode(Int.self, forKey: .sgv)
-        } catch {
-            // The nightscout API returns a double instead of an int
-            sgv = Int(try container.decode(Double.self, forKey: .sgv))
+        sgv = try? container.decodeIfPresent(Int.self, forKey: .sgv)
+        if sgv == nil {
+            // The nightscout API might return a double instead of an int, or the key might be missing
+            if let doubleValue = try? container.decodeIfPresent(Double.self, forKey: .sgv) {
+                sgv = Int(doubleValue)
+            }
+            // If both attempts fail, sgv remains nil
         }
 
         direction = try container.decodeIfPresent(Direction.self, forKey: .direction)

+ 0 - 32
Trio/Sources/Models/Glucose.swift

@@ -1,32 +0,0 @@
-import Foundation
-
-struct Glucose: JSON {
-    let sgv: Int?
-    let glucose: Int?
-    let type: GlucoseType
-    let noise: Int?
-    let date: Date
-    let filtered: Double?
-    let direction: Direction?
-}
-
-enum GlucoseType: String, JSON {
-    case sgv
-    case cal
-    case manual = "Manual"
-}
-
-enum Direction: String, JSON {
-    case tripleUp = "TripleUp"
-    case doubleUp = "DoubleUp"
-    case singleUp = "SingleUp"
-    case fortyFiveUp = "FortyFiveUp"
-    case flat = "Flat"
-    case fortyFiveDown = "FortyFiveDown"
-    case singleDown = "SingleDown"
-    case doubleDown = "DoubleDown"
-    case tripleDown = "TripleDown"
-    case none = "NONE"
-    case notComputable = "NOT COMPUTABLE"
-    case rateOutOfRange = "RATE OUT OF RANGE"
-}

+ 19 - 6
Trio/Sources/Modules/Main/MainStateModel.swift

@@ -6,6 +6,7 @@ import Swinject
 
 extension Main {
     final class StateModel: BaseStateModel<Provider> {
+        @Injected() private var apsManager: APSManager!
         @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
         @Injected() var broadcaster: Broadcaster!
         private(set) var modal: Modal?
@@ -204,24 +205,36 @@ extension Main {
         /*
           Reclassification is needed for Medtronic pumps for 'Pump error:' RileyLink related messages.
           For details, see https://discord.com/channels/1020905149037813862/1338245444186279946/1343469793013141525.
-          Reclassification of Info type messages is based on APSManager.APSError enum values.
-          Currently, we only re-classify APSError.pumpError 'Pump error:' type to MessageType.error.
+          These messages are repeatedly displayed causing users to simply ignore them.
+          Reclassification of these Info type messages is based on APSManager.APSError enum values.
+          We reclassify APSError.pumpError and APSError.invalidPumpState as MessageType.info and MessageSubtype.pump.
+          This allows the user to disable these messages using using the 'Trio Notification' -> 'Always Notify Pump' setting.
           MessageType.error messagges are always displayed to the user and the user cannot disable them.
           Other APSManager.APSError remain as MessageType.info which allows users to disable them
           using the 'Trio Notification' -> 'Always Notify Algorithm' setting.
          */
+
         func reclassifyInfoNotification(_ message: inout MessageContent) {
             if message.title == "" {
                 switch message.type {
                 case .info:
-                    if let errorIndex = message.content.range(of: "error", options: .caseInsensitive) {
+                    if message.content.range(of: "error", options: .caseInsensitive) != nil || message.content
+                        .range(of: String(localized: "Error"), options: .caseInsensitive) != nil
+                    {
                         message.title = String(localized: "Error", comment: "Error title")
-                        if let errorPumpIndex = message.content.range(of: "Pump error:", options: .caseInsensitive) {
-                            message.type = .error
-                        }
                     } else {
                         message.title = String(localized: "Info", comment: "Info title")
                     }
+                    if APSError.pumpWarningMatches(message: message.content) {
+                        message.subtype = .pump
+                        let lastLoopMinutes = Int((Date().timeIntervalSince(apsManager.lastLoopDate) - 30) / 60) + 1
+                        if lastLoopMinutes > 10 {
+                            message.type = .error
+                        }
+                    } else if APSError.pumpErrorMatches(message: message.content) {
+                        message.subtype = .pump
+                        message.type = .error
+                    }
                 case .warning:
                     message.title = String(localized: "Warning", comment: "Warning title")
                 case .error:

+ 1 - 7
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -23,10 +23,6 @@ extension Onboarding {
         var diagnosticsSharingOption: DiagnosticsSharingOption = .enabled
         var hasAcceptedPrivacyPolicy: Bool = false
 
-        // MARK: - Important Startup Notes
-
-        var hasReadImportantStartupNotes: Bool = false
-
         // MARK: - Nightscout Setup
 
         var nightscoutSetupOption: NightscoutSetupOption = .noSelection
@@ -102,9 +98,7 @@ extension Onboarding {
         var maxCOB: Decimal = 120
         var minimumSafetyThreshold: Decimal = 60
 
-        // MARK: - Algorithm Settings Defaults & State
-
-        var hasReadAlgorithmSetupInformation: Bool = false
+        // MARK: - Algorithm Settings Defaults
 
         // Autosens Settings
         var autosensMin: Decimal = 0.7

+ 290 - 126
Trio/Sources/Modules/Onboarding/View/OnboardingRootView.swift

@@ -10,13 +10,44 @@ extension Onboarding {
         let onboardingManager: OnboardingManager
 
         // Step management
+        @State private var currentChapter: OnboardingChapter = .prepareTrio
+        @State private var showingChapterCompletion: OnboardingChapter? = nil
+
         @State private var currentStep: OnboardingStep = .welcome
+        @State private var currentStartupSubstep: StartupSubstep = .startupGuide
         @State private var currentNightscoutSubstep: NightscoutSubstep = .setupSelection
         @State private var currentDeliverySubstep: DeliveryLimitSubstep = .maxIOB
+        @State private var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep = .contents
         @State private var currentAutosensSubstep: AutosensSettingsSubstep = .autosensMin
         @State private var currentSMBSubstep: SMBSettingsSubstep = .enableSMBAlways
         @State private var currentTargetBehaviorSubstep: TargetBehaviorSubstep = .highTempTargetRaisesSensitivity
 
+        private func updateCurrentChapter() {
+            switch currentStep {
+            case .diagnostics,
+                 .nightscout,
+                 .unitSelection:
+                currentChapter = .prepareTrio
+            case .basalRates,
+                 .carbRatio,
+                 .glucoseTarget,
+                 .insulinSensitivity:
+                currentChapter = .therapySettings
+            case .deliveryLimits:
+                currentChapter = .deliveryLimits
+            case .algorithmSettings,
+                 .autosensSettings,
+                 .smbSettings,
+                 .targetBehavior:
+                currentChapter = .algorithmSettings
+            case .bluetooth,
+                 .notifications:
+                currentChapter = .permissionRequests
+            default:
+                break
+            }
+        }
+
         // Animation states
         @State private var animationScale: CGFloat = 1.0
         @State private var animationOpacity: Double = 0
@@ -38,17 +69,13 @@ extension Onboarding {
 
         // Next button conditional
         private var shouldDisableNextButton: Bool {
-            (currentStep == .startupGuide && !state.hasReadImportantStartupNotes)
-                ||
-                (currentStep == .diagnostics && state.diagnosticsSharingOption == .enabled && !state.hasAcceptedPrivacyPolicy)
+            (currentStep == .diagnostics && state.diagnosticsSharingOption == .enabled && !state.hasAcceptedPrivacyPolicy)
                 ||
                 (currentStep == .nightscout && didSelectNightscoutSetupOption)
                 ||
                 (currentStep == .nightscout && hasValidNightscoutConnection)
                 ||
                 (currentStep == .nightscout && didSelectNightscoutImportOption)
-                ||
-                (currentStep == .algorithmSettings && !state.hasReadAlgorithmSetupInformation)
         }
 
         var body: some View {
@@ -66,11 +93,14 @@ extension Onboarding {
                         if (nonInfoOnboardingSteps + [OnboardingStep.overview, OnboardingStep.completed]).contains(currentStep) {
                             // Progress bar
                             OnboardingProgressBar(
+                                currentChapter: currentChapter,
+                                shouldDisplayChapterTitle: showingChapterCompletion == nil,
                                 currentStep: currentStep,
                                 currentSubstep: {
                                     switch currentStep {
                                     case .deliveryLimits: return currentDeliverySubstep.rawValue
                                     case .nightscout: return currentNightscoutSubstep.rawValue
+                                    case .algorithmSettings: return currentAlgorithmSettingsOverviewSubstep.rawValue
                                     case .autosensSettings: return currentAutosensSubstep.rawValue
                                     case .smbSettings: return currentSMBSubstep.rawValue
                                     case .targetBehavior: return currentTargetBehaviorSubstep.rawValue
@@ -80,6 +110,7 @@ extension Onboarding {
                                 stepsWithSubsteps: [
                                     .nightscout: NightscoutSubstep.allCases.count,
                                     .deliveryLimits: DeliveryLimitSubstep.allCases.count,
+                                    .algorithmSettings: AlgorithmSettingsOverviewSubstep.allCases.count,
                                     .autosensSettings: state.filteredAutosensSettingsSubsteps.count,
                                     .smbSettings: SMBSettingsSubstep.allCases.count,
                                     .targetBehavior: TargetBehaviorSubstep.allCases.count
@@ -94,8 +125,11 @@ extension Onboarding {
 
                         OnboardingStepContent(
                             currentStep: $currentStep,
+                            showingChapterCompletion: $showingChapterCompletion,
+                            currentStartupSubstep: $currentStartupSubstep,
                             currentNightscoutSubstep: $currentNightscoutSubstep,
                             currentDeliverySubstep: $currentDeliverySubstep,
+                            currentAlgorithmSettingsOverviewSubstep: $currentAlgorithmSettingsOverviewSubstep,
                             currentAutosensSubstep: $currentAutosensSubstep,
                             currentSMBSubstep: $currentSMBSubstep,
                             currentTargetBehaviorSubstep: $currentTargetBehaviorSubstep,
@@ -107,8 +141,11 @@ extension Onboarding {
 
                         OnboardingNavigationButtons(
                             currentStep: $currentStep,
+                            showingChapterCompletion: $showingChapterCompletion,
+                            currentStartupSubstep: $currentStartupSubstep,
                             currentNightscoutSubstep: $currentNightscoutSubstep,
                             currentDeliverySubstep: $currentDeliverySubstep,
+                            currentAlgorithmSettingsOverviewSubstep: $currentAlgorithmSettingsOverviewSubstep,
                             currentAutosensSubstep: $currentAutosensSubstep,
                             currentSMBSubstep: $currentSMBSubstep,
                             currentTargetBehaviorSubstep: $currentTargetBehaviorSubstep,
@@ -135,6 +172,8 @@ extension Onboarding {
                     }
                     isAnimating = true
                 }
+
+                updateCurrentChapter()
             }
             .onAppear(perform: configureView)
         }
@@ -143,34 +182,62 @@ extension Onboarding {
 
 /// A progress bar that shows the user's progress through the onboarding process.
 struct OnboardingProgressBar: View {
+    let currentChapter: OnboardingChapter
+    let shouldDisplayChapterTitle: Bool
     let currentStep: OnboardingStep
     let currentSubstep: Int?
     let stepsWithSubsteps: [OnboardingStep: Int]
     let nightscoutSetupOption: NightscoutSetupOption
 
+    private let capsuleSize = CGFloat(UIFont.preferredFont(forTextStyle: .subheadline).pointSize) * 1.3
+
+    private var shouldShowCurrentChapter: Bool {
+        shouldDisplayChapterTitle && currentStep != .overview && currentStep != .completed
+    }
+
     var body: some View {
-        HStack(spacing: 4) {
-            ForEach(renderedSteps, id: \.id) { step in
-                ZStack(alignment: .leading) {
-                    Rectangle()
-                        .fill(Color.gray.opacity(0.3))
-                        .frame(height: 4)
-                        .cornerRadius(2)
-
-                    GeometryReader { geo in
+        VStack(alignment: .leading, spacing: 10) {
+            // only show this for the actual chapters, not the overview of chapters or completed view
+            if shouldShowCurrentChapter {
+                HStack(spacing: CGFloat(UIFont.preferredFont(forTextStyle: .subheadline).pointSize)) {
+                    Text("\(currentChapter.rawValue + 1)")
+                        .font(.subheadline)
+                        .fontWeight(.heavy)
+                        .frame(width: capsuleSize, height: capsuleSize, alignment: .center)
+                        .background(Color.blue)
+                        .foregroundStyle(Color.bgDarkBlue)
+                        .clipShape(Capsule())
+                    Text(currentChapter.title)
+                        .font(.subheadline)
+                        .kerning(capsuleSize / 4)
+                        .textCase(.uppercase)
+                        .bold()
+                        .foregroundStyle(Color.secondary)
+                }
+            }
+
+            HStack(spacing: 4) {
+                ForEach(renderedSteps, id: \.id) { step in
+                    ZStack(alignment: .leading) {
                         Rectangle()
-                            .fill(Color.blue)
-                            .frame(
-                                width: geo.size.width * fillFraction(for: step.step, totalSubsteps: step.substeps),
-                                height: 4
-                            )
+                            .fill(Color.gray.opacity(0.3))
+                            .frame(height: 4)
                             .cornerRadius(2)
+
+                        GeometryReader { geo in
+                            Rectangle()
+                                .fill(Color.blue)
+                                .frame(
+                                    width: geo.size.width * fillFraction(for: step.step, totalSubsteps: step.substeps),
+                                    height: 4
+                                )
+                                .cornerRadius(2)
+                        }
                     }
+                    .frame(height: 4)
                 }
-                .frame(height: 4)
             }
-        }
-        .padding(.horizontal)
+        }.padding(.horizontal)
     }
 
     private var renderedSteps: [(id: String, step: OnboardingStep, substeps: Int?)] {
@@ -214,8 +281,11 @@ struct OnboardingProgressBar: View {
 
 struct OnboardingStepContent: View {
     @Binding var currentStep: OnboardingStep
+    @Binding var showingChapterCompletion: OnboardingChapter?
+    @Binding var currentStartupSubstep: StartupSubstep
     @Binding var currentNightscoutSubstep: NightscoutSubstep
     @Binding var currentDeliverySubstep: DeliveryLimitSubstep
+    @Binding var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep
     @Binding var currentAutosensSubstep: AutosensSettingsSubstep
     @Binding var currentSMBSubstep: SMBSettingsSubstep
     @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
@@ -228,126 +298,154 @@ struct OnboardingStepContent: View {
                 VStack(alignment: .leading, spacing: 20) {
                     Color.clear.frame(height: 0).id("top")
 
-                    if currentStep != .welcome && currentStep != .completed {
-                        HStack {
-                            if currentStep == .nightscout {
-                                Image(currentStep.iconName)
-                                    .resizable()
-                                    .scaledToFit()
-                                    .frame(width: 60, height: 60)
-                            } else if currentStep == .bluetooth {
-                                Image(currentStep.iconName)
-                                    .font(.system(size: 40))
-                                    .foregroundColor(currentStep.accentColor)
-                                    .frame(width: 60, height: 60)
-                                    .background(
-                                        Circle()
-                                            .fill(currentStep.accentColor.opacity(0.2))
-                                    )
-                            } else {
-                                Image(systemName: currentStep.iconName)
-                                    .font(.system(size: 40))
-                                    .foregroundColor(currentStep.accentColor)
-                                    .frame(width: 60, height: 60)
-                                    .background(
-                                        Circle()
-                                            .fill(currentStep.accentColor.opacity(0.2))
-                                    )
-                            }
-
-                            VStack(alignment: .leading) {
-                                Text(currentStep.title)
-                                    .font(.title)
-                                    .fontWeight(.bold)
-                                    .foregroundColor(.primary)
-
-                                Text(currentStep.description)
-                                    .font(.subheadline)
-                                    .foregroundColor(.secondary)
-                                    .fixedSize(horizontal: false, vertical: true)
-                            }
-                        }
-                        .padding([.horizontal, .top])
+                    if currentStep != .welcome, currentStep != .completed, showingChapterCompletion == nil {
+                        contentHeader
                     }
 
-                    Group {
-                        switch currentStep {
-                        case .welcome:
-                            WelcomeStepView()
-                        case .startupGuide:
-                            StartupGuideStepView(state: state)
-                        case .overview:
-                            OverviewStepView()
-                        case .diagnostics:
-                            DiagnosticsStepView(state: state)
-                        case .nightscout:
-                            switch currentNightscoutSubstep {
-                            case .setupSelection:
-                                NightscoutSetupStepView(state: state)
-                            case .connectToNightscout:
-                                NightscoutLoginStepView(state: state)
-                            case .importFromNightscout:
-                                NightscoutImportStepView(state: state)
+                    if let chapter = showingChapterCompletion {
+                        CompletedStepView(isOnboardingCompleted: false, currentChapter: chapter)
+                    } else {
+                        Group {
+                            switch currentStep {
+                            case .welcome:
+                                WelcomeStepView()
+                            case .startupInfo:
+                                switch currentStartupSubstep {
+                                case .startupGuide:
+                                    StartupGuideStepView(state: state)
+                                case .returningUser:
+                                    StartupReturningUserStepView(state: state)
+                                case .forceCloseWarning:
+                                    StartupForceCloseWarningStepView(state: state)
+                                }
+                            case .overview:
+                                OverviewStepView()
+                            case .diagnostics:
+                                DiagnosticsStepView(state: state)
+                            case .nightscout:
+                                switch currentNightscoutSubstep {
+                                case .setupSelection:
+                                    NightscoutSetupStepView(state: state)
+                                case .connectToNightscout:
+                                    NightscoutLoginStepView(state: state)
+                                case .importFromNightscout:
+                                    NightscoutImportStepView(state: state)
+                                }
+                            case .unitSelection:
+                                UnitSelectionStepView(state: state)
+                            case .glucoseTarget:
+                                GlucoseTargetStepView(state: state)
+                            case .basalRates:
+                                BasalProfileStepView(state: state)
+                            case .carbRatio:
+                                CarbRatioStepView(state: state)
+                            case .insulinSensitivity:
+                                InsulinSensitivityStepView(state: state)
+                            case .deliveryLimits:
+                                DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
+                            case .algorithmSettings:
+                                switch currentAlgorithmSettingsOverviewSubstep {
+                                case .contents:
+                                    AlgorithmSettingsContentsStepView(state: state)
+                                case .importantNotes:
+                                    AlgorithmSettingsImportantNotesStepView(state: state)
+                                }
+                            case .autosensSettings:
+                                AlgorithmSettingsSubstepView(state: state, substep: currentAutosensSubstep)
+                            case .smbSettings:
+                                AlgorithmSettingsSubstepView(state: state, substep: currentSMBSubstep)
+                            case .targetBehavior:
+                                AlgorithmSettingsSubstepView(state: state, substep: currentTargetBehaviorSubstep)
+                            case .notifications:
+                                NotificationPermissionStepView(state: state, currentStep: $currentStep)
+                            case .bluetooth:
+                                BluetoothPermissionStepView(
+                                    state: state,
+                                    bluetoothManager: state.bluetoothManager,
+                                    currentStep: $currentStep
+                                )
+                            case .completed:
+                                CompletedStepView(isOnboardingCompleted: true, currentChapter: nil)
                             }
-                        case .unitSelection:
-                            UnitSelectionStepView(state: state)
-                        case .glucoseTarget:
-                            GlucoseTargetStepView(state: state)
-                        case .basalRates:
-                            BasalProfileStepView(state: state)
-                        case .carbRatio:
-                            CarbRatioStepView(state: state)
-                        case .insulinSensitivity:
-                            InsulinSensitivityStepView(state: state)
-                        case .deliveryLimits:
-                            DeliveryLimitsStepView(state: state, substep: currentDeliverySubstep)
-                        case .algorithmSettings:
-                            AlgorithmSettingsStepView(state: state)
-                        case .autosensSettings:
-                            AlgorithmSettingsSubstepView(state: state, substep: currentAutosensSubstep)
-                        case .smbSettings:
-                            AlgorithmSettingsSubstepView(state: state, substep: currentSMBSubstep)
-                        case .targetBehavior:
-                            AlgorithmSettingsSubstepView(state: state, substep: currentTargetBehaviorSubstep)
-                        case .notifications:
-                            NotificationPermissionStepView(state: state, currentStep: $currentStep)
-                        case .bluetooth:
-                            BluetoothPermissionStepView(
-                                state: state,
-                                bluetoothManager: state.bluetoothManager,
-                                currentStep: $currentStep
-                            )
-                        case .completed:
-                            CompletedStepView()
                         }
+                        .transition(
+                            navigationDirection == .forward
+                                ? .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
+                                : .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
+                        )
+                        .padding(.horizontal)
+                        .id(currentStep.id)
                     }
-                    .transition(
-                        navigationDirection == .forward
-                            ? .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
-                            : .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
-                    )
-                    .padding(.horizontal)
-                    .id(currentStep.id)
                 }
                 .padding(.bottom, 80)
             }
             .onChange(of: currentStep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
+            .onChange(of: currentStartupSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
             .onChange(of: currentNightscoutSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
             .onChange(of: currentDeliverySubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
+            .onChange(of: currentAlgorithmSettingsOverviewSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
+            .onChange(of: currentAutosensSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
+            .onChange(of: currentSMBSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
+            .onChange(of: currentTargetBehaviorSubstep) { _, _ in scrollProxy.scrollTo("top", anchor: .top) }
             .safeAreaInset(edge: .top) {
                 // avoid letting content scroll beneath the status bar / dynamic island for content views with not progress bar (which adds top spacing)
-                if currentStep == .startupGuide || currentStep == .completed {
+                if currentStep == .startupInfo || currentStep == .completed {
                     Color.clear.frame(height: 0)
                 }
             }
         }
     }
+
+    private var contentHeader: some View {
+        HStack {
+            if currentStep == .nightscout {
+                Image(currentStep.iconName)
+                    .resizable()
+                    .scaledToFit()
+                    .frame(width: 60, height: 60)
+            } else if currentStep == .bluetooth {
+                Image(currentStep.iconName)
+                    .font(.system(size: 40))
+                    .foregroundColor(currentStep.accentColor)
+                    .frame(width: 60, height: 60)
+                    .background(
+                        Circle()
+                            .fill(currentStep.accentColor.opacity(0.2))
+                    )
+            } else {
+                Image(systemName: currentStep.iconName)
+                    .font(.system(size: 40))
+                    .foregroundColor(currentStep.accentColor)
+                    .frame(width: 60, height: 60)
+                    .background(
+                        Circle()
+                            .fill(currentStep.accentColor.opacity(0.2))
+                    )
+            }
+
+            VStack(alignment: .leading) {
+                Text(currentStep.title)
+                    .font(.title)
+                    .fontWeight(.bold)
+                    .foregroundColor(.primary)
+
+                Text(currentStep.description)
+                    .font(.subheadline)
+                    .foregroundColor(.secondary)
+                    .fixedSize(horizontal: false, vertical: true)
+            }
+        }
+        .padding(.horizontal)
+    }
 }
 
 struct OnboardingNavigationButtons: View {
     @Binding var currentStep: OnboardingStep
+    @Binding var showingChapterCompletion: OnboardingChapter?
+    @Binding var currentStartupSubstep: StartupSubstep
     @Binding var currentNightscoutSubstep: NightscoutSubstep
     @Binding var currentDeliverySubstep: DeliveryLimitSubstep
+    @Binding var currentAlgorithmSettingsOverviewSubstep: AlgorithmSettingsOverviewSubstep
     @Binding var currentAutosensSubstep: AutosensSettingsSubstep
     @Binding var currentSMBSubstep: SMBSettingsSubstep
     @Binding var currentTargetBehaviorSubstep: TargetBehaviorSubstep
@@ -400,10 +498,26 @@ struct OnboardingNavigationButtons: View {
     // MARK: - Navigation Logic
 
     private func handleBackNavigation() {
+        if showingChapterCompletion != nil {
+            showingChapterCompletion = nil
+            return
+        }
+
         switch currentStep {
-        case .completed:
-            currentStep = .targetBehavior
-            currentTargetBehaviorSubstep = .halfBasalTarget
+        case .startupInfo:
+            if let previousSub = StartupSubstep(rawValue: currentStartupSubstep.rawValue - 1) {
+                currentStartupSubstep = previousSub
+            } else if let previous = currentStep.previous {
+                currentStep = previous
+                currentStartupSubstep = .startupGuide
+            }
+
+        case .overview:
+            currentStartupSubstep = .forceCloseWarning
+
+            if let previous = currentStep.previous {
+                currentStep = previous
+            }
 
         case .nightscout:
             if currentNightscoutSubstep == .setupSelection,
@@ -424,7 +538,12 @@ struct OnboardingNavigationButtons: View {
             }
 
         case .algorithmSettings:
-            if let previous = currentStep.previous {
+            if let previousSub = AlgorithmSettingsOverviewSubstep(
+                rawValue: currentAlgorithmSettingsOverviewSubstep
+                    .rawValue - 1
+            ) {
+                currentAlgorithmSettingsOverviewSubstep = previousSub
+            } else if let previous = currentStep.previous {
                 currentStep = previous
                 currentDeliverySubstep = .minimumSafetyThreshold
                 currentAutosensSubstep = .autosensMin
@@ -442,6 +561,8 @@ struct OnboardingNavigationButtons: View {
             }
 
         case .smbSettings:
+            currentAlgorithmSettingsOverviewSubstep = .importantNotes
+
             if let previous = SMBSettingsSubstep(rawValue: currentSMBSubstep.rawValue - 1) {
                 /// If user has activated setting `.enableSMBAlways`, when navigating backwards
                 /// skip other redundant "Enable SMB"-settings and go straight to `enableSMBAlways`
@@ -474,6 +595,16 @@ struct OnboardingNavigationButtons: View {
                 currentSMBSubstep = .maxDeltaGlucoseThreshold
             }
 
+        case .notifications:
+            currentTargetBehaviorSubstep = .halfBasalTarget
+
+            if let previous = currentStep.previous {
+                currentStep = previous
+            }
+
+        case .completed:
+            currentStep = .bluetooth
+
         default:
             if let previous = currentStep.previous {
                 currentStep = previous
@@ -482,7 +613,28 @@ struct OnboardingNavigationButtons: View {
     }
 
     private func handleNextNavigation() {
+        if showingChapterCompletion != nil {
+            showingChapterCompletion = nil
+            if let next = currentStep.next {
+                currentStep = next
+            }
+            return
+        }
+
+        if let chapter = currentStep.chapterCompletion {
+            showingChapterCompletion = chapter
+            return
+        }
+
         switch currentStep {
+        case .startupInfo:
+            if let next = StartupSubstep(rawValue: currentStartupSubstep.rawValue + 1) {
+                currentStartupSubstep = next
+            } else if let nextStep = currentStep.next {
+                currentStep = nextStep
+                currentStartupSubstep = .startupGuide
+            }
+
         case .nightscout:
             if currentNightscoutSubstep != .importFromNightscout {
                 if currentNightscoutSubstep == .setupSelection,
@@ -506,9 +658,19 @@ struct OnboardingNavigationButtons: View {
         case .deliveryLimits:
             if let next = DeliveryLimitSubstep(rawValue: currentDeliverySubstep.rawValue + 1) {
                 currentDeliverySubstep = next
+            } else {
+                /// Setting delivery substep to the last substep (`.minimumSafetyThreshold`) and `showingChapterCompletion` to non-`nil`
+                /// prompts display of chapter completion screen; if user navigates back, it stays at correct substep.
+                currentDeliverySubstep = .minimumSafetyThreshold
+                showingChapterCompletion = .deliveryLimits
+            }
+
+        case .algorithmSettings:
+            if let next = AlgorithmSettingsOverviewSubstep(rawValue: currentAlgorithmSettingsOverviewSubstep.rawValue + 1) {
+                currentAlgorithmSettingsOverviewSubstep = next
             } else if let nextStep = currentStep.next {
                 currentStep = nextStep
-                currentDeliverySubstep = .maxIOB
+                currentAlgorithmSettingsOverviewSubstep = .contents
             }
 
         case .autosensSettings:
@@ -540,9 +702,11 @@ struct OnboardingNavigationButtons: View {
         case .targetBehavior:
             if let next = TargetBehaviorSubstep(rawValue: currentTargetBehaviorSubstep.rawValue + 1) {
                 currentTargetBehaviorSubstep = next
-            } else if let nextStep = currentStep.next {
-                currentStep = nextStep
-                currentTargetBehaviorSubstep = .highTempTargetRaisesSensitivity
+            } else {
+                /// Setting target behavior substep to the last substep (`.halfBasalTarget`) and `showingChapterCompletion` to non-`nil`
+                /// prompts display of chapter completion screen; if user navigates back, it stays at correct substep.
+                currentTargetBehaviorSubstep = .halfBasalTarget
+                showingChapterCompletion = .algorithmSettings
             }
 
         case .notifications:

+ 63 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsContentsStepView.swift

@@ -0,0 +1,63 @@
+//
+//  AlgorithmSettingsContentsStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 14.04.25
+//
+import SwiftUI
+
+struct AlgorithmSettingsContentsStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Configure the algorithm…")
+                .padding(.horizontal)
+                .font(.title3)
+                .bold()
+
+            VStack(alignment: .leading, spacing: 10) {
+                HStack(alignment: .top, spacing: 10) {
+                    Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(Color.bgDarkBlue, Color.orange)
+                        .symbolRenderingMode(.palette)
+                    Text("Important").foregroundStyle(Color.orange)
+                }.bold()
+
+                Text("Our strong recommendation is to ")
+                    + Text("leave everything on default").bold()
+                    + Text(" as a beginner.")
+            }
+            .frame(maxWidth: .infinity)
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .overlay(
+                RoundedRectangle(cornerRadius: 10)
+                    .stroke(Color.orange, lineWidth: 2)
+            )
+            .cornerRadius(10)
+
+            VStack(alignment: .leading, spacing: 20) {
+                Text(
+                    "Trio can automatically adapt insulin delivery based on inputs and glucose forecasts. Your algorithm settings play a major part in accurate and effective dosing."
+                ).multilineTextAlignment(.leading)
+
+                VStack(alignment: .leading, spacing: 10) {
+                    Text("In the next few steps, you’ll configure your algorithm settings for")
+                        .font(.headline)
+                        .padding(.bottom, 4)
+                        .multilineTextAlignment(.leading)
+
+                    BulletPoint(String(localized: "Autosens"))
+                    BulletPoint(String(localized: "Super Micro Bolus (SMB)"))
+                    BulletPoint(String(localized: "Target Behavior"))
+                }
+
+                Text("Only adjust these settings if you’re an advanced or returning user who knows what they’re doing.")
+                    .multilineTextAlignment(.leading)
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+        }
+    }
+}

+ 62 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsImportantNotesStepView.swift

@@ -0,0 +1,62 @@
+//
+//  AlgorithmSettingsImportantNotesStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 14.04.25
+//
+import SwiftUI
+
+struct AlgorithmSettingsImportantNotesStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("A few important notes…")
+                .padding(.horizontal)
+                .font(.title3)
+                .bold()
+
+            VStack(alignment: .leading, spacing: 10) {
+                HStack(alignment: .top, spacing: 10) {
+                    Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(Color.bgDarkBlue, Color.orange)
+                        .symbolRenderingMode(.palette)
+                    Text("Important").foregroundStyle(Color.orange)
+                }.bold()
+
+                Text("Dynamic ISF requires at least ") + Text("7 days")
+                    .bold() + Text(" of usage data and is not yet configurable.")
+            }
+            .frame(maxWidth: .infinity)
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .overlay(
+                RoundedRectangle(cornerRadius: 10)
+                    .stroke(Color.orange, lineWidth: 2)
+            )
+            .cornerRadius(10)
+
+            VStack(alignment: .leading, spacing: 10) {
+                Text("Some helpful reminders:")
+                    .font(.headline)
+                    .padding(.bottom, 4)
+                    .multilineTextAlignment(.leading)
+
+                BulletPoint(
+                    String(
+                        localized: "Even if you’re an updating user, you’ll be guided through the algorithm settings configuration step-by-step."
+                    )
+                )
+                BulletPoint(String(localized: "All additional \"advanced settings\" have been reset."))
+                BulletPoint(
+                    String(localized: "The duration of insulin action (DIA) is now locked to Trio’s new default of 10 hours.")
+                )
+                BulletPoint(
+                    String(localized: "We strongly recommend not changing DIA — it’s essential to stable and safe operation.")
+                )
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+        }
+    }
+}

+ 0 - 70
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/AlgorithmSettings/AlgorithmSettingsStepView.swift

@@ -1,70 +0,0 @@
-//
-//  AlgorithmSettingsStepView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 14.04.25
-//
-import SwiftUI
-
-struct AlgorithmSettingsStepView: View {
-    @Bindable var state: Onboarding.StateModel
-
-    @State private var shouldDisplayPicker: Bool = false
-    @State private var decimalPlaceholder: Decimal = 0.0
-    @State private var booleanPlaceholder: Bool = false
-
-    private let settingsProvider = PickerSettingsProvider.shared
-
-    var body: some View {
-        VStack(alignment: .leading, spacing: 20) {
-            Text("Configure the algorithm…")
-                .padding(.horizontal)
-                .font(.title3)
-                .bold()
-
-            VStack(alignment: .leading, spacing: 10) {
-                Text(
-                    "Trio can automatically adapt insulin delivery based on inputs and glucose forecasts. Your algorithm settings play a major part in accurate and effective dosing."
-                ).multilineTextAlignment(.leading)
-
-                Text("In the next few steps, you’ll configure your algorithm settings for")
-                BulletPoint(String(localized: "Autosens"))
-                BulletPoint(String(localized: "Super Micro Bolus (SMB)"))
-                BulletPoint(String(localized: "Target Behavior"))
-
-                Text("Our strong recommendation is to ")
-                    + Text("leave everything on default").bold()
-                    + Text(" as a beginner.")
-
-                Text("Only adjust these settings if you’re an advanced or returning user who knows what they’re doing.")
-                    .multilineTextAlignment(.leading)
-            }
-            .padding(.horizontal)
-
-            VStack(alignment: .leading, spacing: 10) {
-                Text("A few important notes:")
-                    .font(.headline)
-                    .padding(.bottom, 4)
-
-                BulletPoint(String(localized: "Dynamic ISF requires at least 7 days of usage data and is not yet configurable."))
-                BulletPoint(String(localized: "Even if you’re an updating user, you’ll be guided through this step-by-step."))
-                BulletPoint(String(localized: "All additional \"advanced settings\" have been reset."))
-                BulletPoint(
-                    String(localized: "The duration of insulin action (DIA) is now locked to Trio’s new default of 10 hours.")
-                )
-                BulletPoint(
-                    String(localized: "We strongly recommend not changing DIA — it’s essential to stable and safe operation.")
-                )
-            }
-            .padding(.horizontal)
-
-            Divider()
-
-            Toggle(isOn: $state.hasReadAlgorithmSetupInformation) {
-                Text("Got it! I'm ready to continue.").padding(.leading, 6).bold()
-            }
-            .toggleStyle(CheckboxToggleStyle(tint: Color.blue))
-            .padding(.horizontal)
-        }
-    }
-}

+ 52 - 65
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CompletedStepView.swift

@@ -2,91 +2,75 @@ import SwiftUI
 
 /// Completed step view shown at the end of onboarding.
 struct CompletedStepView: View {
+    let isOnboardingCompleted: Bool
+    let currentChapter: OnboardingChapter?
+
     var body: some View {
         VStack(alignment: .center, spacing: 20) {
-            Image(systemName: "checkmark.circle.fill")
-                .font(.system(size: 60))
-                .foregroundColor(.green)
-
-            Text("You're All Set!")
-                .font(.title)
-                .fontWeight(.bold)
+            if isOnboardingCompleted {
+                Image(systemName: "checkmark.circle.fill")
+                    .font(.system(size: 60))
+                    .foregroundColor(.green)
+
+                Text("You're All Set!")
+                    .font(.title)
+                    .fontWeight(.bold)
+                    .multilineTextAlignment(.center)
+
+                Text(
+                    "You've successfully completed the initial setup of Trio. Tap 'Get Started' to save your settings and start using Trio."
+                )
                 .multilineTextAlignment(.center)
-
-            Text(
-                "You've successfully completed the initial setup of Trio. Tap 'Get Started' to save your settings and start using Trio."
-            )
-            .multilineTextAlignment(.center)
-            .foregroundColor(.secondary)
+                .foregroundColor(.secondary)
+            }
 
             VStack(alignment: .leading, spacing: 12) {
-                completedItemsView(
-                    stepIndex: 1,
-                    title: String(localized: "Prepare Trio"),
-                    description: String(
-                        localized: "App diagnostics sharing, Nightscout setup, and unit and pump model selection are all complete."
+                ForEach(Array(OnboardingChapter.allCases.enumerated()), id: \.element.id) { index, chapter in
+                    completedItemsView(
+                        stepIndex: index + 1,
+                        title: chapter.title,
+                        description: chapter.completedDescription,
+                        isCompleted: isChapterCompleted(chapter)
                     )
-                )
-
-                Divider()
 
-                completedItemsView(
-                    stepIndex: 2,
-                    title: String(localized: "Therapy Settings"),
-                    description: String(
-                        localized: "Glucose target, basal rates, carb ratios, and insulin sensitivity match your needs."
-                    )
-                )
-
-                Divider()
-
-                completedItemsView(
-                    stepIndex: 3,
-                    title: String(localized: "Delivery Limits"),
-                    description: String(
-                        localized: "Safety boundaries for insulin delivery and carb entries are set to help Trio keep you safe."
-                    )
-                )
-
-                Divider()
-
-                completedItemsView(
-                    stepIndex: 4,
-                    title: String(localized: "Algorithm Settings"),
-                    description: String(localized: "Trio’s algorithm features are customized to fit your preferences and needs.")
-                )
-
-                Divider()
-
-                completedItemsView(
-                    stepIndex: 5,
-                    title: String(localized: "Permission Requests"),
-                    description: String(localized: "Notifications and Bluetooth permissions are handled to your liking.")
-                )
+                    if index < (OnboardingChapter.allCases.count - 1) {
+                        Divider()
+                    }
+                }
             }
             .padding()
             .background(Color.green.opacity(0.1))
             .cornerRadius(12)
 
-            Text("Remember, you can adjust these settings at any time in the app settings if needed.")
-                .multilineTextAlignment(.center)
-                .foregroundColor(.primary)
-                .bold()
+            if isOnboardingCompleted {
+                Text("Remember, you can adjust these settings at any time in the app settings if needed.")
+                    .multilineTextAlignment(.center)
+                    .foregroundColor(.primary)
+                    .bold()
+            }
         }
         .padding()
         .frame(maxWidth: .infinity)
     }
 
+    /// Determines if a chapter should be marked as completed
+    private func isChapterCompleted(_ chapter: OnboardingChapter) -> Bool {
+        guard let currentChapter else { return isOnboardingCompleted }
+        if isOnboardingCompleted { return true }
+        return chapter.id <= currentChapter.id
+    }
+
     /// A reusable view for displaying setting items in the completed step.
     @ViewBuilder private func completedItemsView(
         stepIndex: Int,
         title: String,
-        description: String
+        description: String,
+        isCompleted: Bool
     ) -> some View {
         VStack(alignment: .leading, spacing: 10) {
             HStack {
                 HStack(spacing: 14) {
-                    stepCount(stepIndex)
+                    stepCount(stepIndex, isCompleted: isCompleted)
                     Text(title)
                         .font(.headline)
                         .bold()
@@ -94,8 +78,8 @@ struct CompletedStepView: View {
 
                 Spacer()
 
-                Image(systemName: "checkmark")
-                    .foregroundStyle(Color.green)
+                Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
+                    .foregroundStyle(isCompleted ? Color.green : Color.secondary)
                     .font(.headline)
                     .bold()
             }
@@ -108,16 +92,19 @@ struct CompletedStepView: View {
         }
     }
 
-    @ViewBuilder private func stepCount(_ count: Int) -> some View {
+    @ViewBuilder private func stepCount(_ count: Int, isCompleted: Bool) -> some View {
         Text(count.description)
             .font(.subheadline.bold())
             .frame(width: 26, height: 26, alignment: .center)
-            .background(Color.green)
+            .background(isCompleted ? Color.green : Color.secondary)
             .foregroundStyle(Color.bgDarkerDarkBlue)
             .clipShape(Capsule())
     }
 }
 
 #Preview {
-    CompletedStepView()
+    CompletedStepView(
+        isOnboardingCompleted: true,
+        currentChapter: nil
+    )
 }

+ 10 - 50
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OverviewStepView.swift

@@ -14,58 +14,18 @@ struct OverviewStepView: View {
                 .padding(.horizontal)
 
             VStack(alignment: .center, spacing: 12) {
-                overviewItem(
-                    stepIndex: 1,
-                    title: String(localized: "Prepare Trio"),
-                    duration: "3-5",
-                    description: String(
-                        localized: "Configure diagnostics sharing, optionally sync with Nightscout, and enter essentials."
+                ForEach(Array(OnboardingChapter.allCases.enumerated()), id: \.element.id) { index, chapter in
+                    overviewItem(
+                        stepIndex: index + 1,
+                        title: chapter.title,
+                        duration: chapter.duration,
+                        description: chapter.overviewDescription
                     )
-                )
 
-                Divider()
-
-                overviewItem(
-                    stepIndex: 2,
-                    title: String(localized: "Therapy Settings"),
-                    duration: "5-10",
-                    description: String(
-                        localized: "Define your glucose targets, basal rates, carb ratios, and insulin sensitivities."
-                    )
-                )
-
-                Divider()
-
-                overviewItem(
-                    stepIndex: 3,
-                    title: String(localized: "Delivery Limits"),
-                    duration: "3-5",
-                    description: String(
-                        localized: "Set boundaries for insulin delivery and carb entries to help Trio keep you safe."
-                    )
-                )
-
-                Divider()
-
-                overviewItem(
-                    stepIndex: 4,
-                    title: String(localized: "Algorithm Settings"),
-                    duration: "5-10",
-                    description: String(
-                        localized: "Customize Trio’s algorithm features. Most users start with the recommended settings."
-                    )
-                )
-
-                Divider()
-
-                overviewItem(
-                    stepIndex: 5,
-                    title: String(localized: "Permission Requests"),
-                    duration: "1",
-                    description: String(
-                        localized: "Authorize Trio to send notifications and use Bluetooth. You must allow both for Trio to work properly."
-                    )
-                )
+                    if index < (OnboardingChapter.allCases.count - 1) {
+                        Divider()
+                    }
+                }
             }
             .padding()
             .background(Color.chart.opacity(0.65))

+ 53 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuide/StartupForceCloseWarningStepView.swift

@@ -0,0 +1,53 @@
+//
+//  StartupForceCloseWarningStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 27.04.25.
+//
+import SwiftUI
+
+struct StartupForceCloseWarningStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    @Environment(\.openURL) var openURL
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("One last thing, before you begin...")
+                .font(.title3)
+                .bold()
+
+            VStack(alignment: .leading, spacing: 10) {
+                BulletPoint(
+                    String(localized: "You can pause at any time. If you feel like taking a break, do it and put the phone down!")
+                )
+                BulletPoint(
+                    String(
+                        localized: "All entries you made during Onboarding will be saved automatically when you complete the wizard."
+                    )
+                )
+            }
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+
+            VStack(alignment: .leading, spacing: 10) {
+                HStack(alignment: .top, spacing: 10) {
+                    Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(Color.bgDarkBlue, Color.orange)
+                        .symbolRenderingMode(.palette)
+                    Text("Important").foregroundStyle(Color.orange)
+                }.bold()
+
+                Text("Just be aware: if you force quit the app before finishing onboarding, your progress will not be saved.")
+            }
+            .frame(maxWidth: .infinity)
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .overlay(
+                RoundedRectangle(cornerRadius: 10)
+                    .stroke(Color.orange, lineWidth: 2)
+            )
+            .cornerRadius(10)
+        }
+    }
+}

+ 45 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuide/StartupGuideStepView.swift

@@ -0,0 +1,45 @@
+//
+//  StartupGuideStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 06.04.25.
+//
+import SwiftUI
+
+struct StartupGuideStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    @Environment(\.openURL) var openURL
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Before you begin…")
+                .padding(.horizontal)
+                .font(.title3)
+                .bold()
+
+            VStack {
+                VStack(alignment: .leading, spacing: 10) {
+                    BulletPoint(String(localized: "Take a deep breath — you've got this."))
+                    BulletPoint(String(localized: "There's no rush. Take all the time you need."))
+                    BulletPoint(String(localized: "Everything you enter here can be adjusted later in the app."))
+                    BulletPoint(String(localized: "Want a hand? You can open our full Startup Guide here:"))
+                }
+
+                Button {
+                    openURL(URL(string: "https://triodocs.org/startup-guide")!)
+                } label: {
+                    Text("https://triodocs.org/startup-guide")
+                        .padding(.horizontal, 12)
+                        .padding(.vertical, 8)
+                        .background(Color.blue.opacity(0.2))
+                        .cornerRadius(8)
+                }
+                .frame(maxWidth: .infinity, alignment: .center)
+                .padding(.horizontal)
+            }.padding()
+                .background(Color.chart.opacity(0.65))
+                .cornerRadius(10)
+        }
+    }
+}

+ 67 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuide/StartupReturningUserStepView.swift

@@ -0,0 +1,67 @@
+//
+//  StartupReturningUserStepView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 27.04.25.
+//
+import SwiftUI
+
+struct StartupReturningUserStepView: View {
+    @Bindable var state: Onboarding.StateModel
+
+    @Environment(\.openURL) var openURL
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 20) {
+            Text("Already using Trio and updating from an older version?")
+                .padding(.horizontal)
+                .font(.title3)
+                .bold()
+
+            VStack(alignment: .leading, spacing: 10) {
+                HStack(alignment: .top, spacing: 10) {
+                    Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(Color.bgDarkBlue, Color.orange)
+                        .symbolRenderingMode(.palette)
+                    Text("Important").foregroundStyle(Color.orange)
+                }.bold()
+
+                Text("Your treatment data (pump events, carb entries, glucose trace, etc.) are not migrated.")
+
+                Divider().overlay(Color.orange)
+
+                Text("Your algorithm settings (previously called \"OpenAPS settings\") are reset to defaults.")
+            }
+            .frame(maxWidth: .infinity)
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .overlay(
+                RoundedRectangle(cornerRadius: 10)
+                    .stroke(Color.orange, lineWidth: 2)
+            )
+            .cornerRadius(10)
+
+            VStack(alignment: .leading, spacing: 10) {
+                Text("Here's what you can expect to be preserved:")
+                    .font(.headline)
+                    .padding(.bottom, 4)
+
+                BulletPoint(String(localized: "Your pump and CGM configurations are retained and fully functional."))
+                BulletPoint(
+                    String(
+                        localized: "Your therapy settings (basal rates, carb ratios, insulin sensitivities and glucose targets) are carried over."
+                    )
+                )
+                BulletPoint(String(localized: "We recommend reviewing them carefully — Trio will guide you step-by-step."))
+                BulletPoint(
+                    String(
+                        localized: "You will also be guided through re-configuring your algorithm settings, respecting Trio's new guardrails."
+                    )
+                )
+            }
+            .frame(maxWidth: .infinity)
+            .padding()
+            .background(Color.chart.opacity(0.65))
+            .cornerRadius(10)
+        }
+    }
+}

+ 0 - 71
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/StartupGuideStepView.swift

@@ -1,71 +0,0 @@
-//
-//  StartupGuideStepView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 06.04.25.
-//
-import SwiftUI
-
-struct StartupGuideStepView: View {
-    @Bindable var state: Onboarding.StateModel
-
-    @Environment(\.openURL) var openURL
-
-    var body: some View {
-        VStack(alignment: .leading, spacing: 20) {
-            Text("Before you begin…")
-                .padding(.horizontal)
-                .font(.title3)
-                .bold()
-
-            VStack(alignment: .leading, spacing: 10) {
-                BulletPoint(String(localized: "Take a deep breath — you've got this."))
-                BulletPoint(String(localized: "There's no rush. Take all the time you need."))
-                BulletPoint(String(localized: "Everything you enter here can be adjusted later in the app."))
-                BulletPoint(String(localized: "Want a hand? You can open our full Startup Guide here:"))
-
-                Button {
-                    openURL(URL(string: "https://triodocs.org/startup-guide")!)
-                } label: {
-                    Text("https://triodocs.org/startup-guide")
-                        .padding(.horizontal, 12)
-                        .padding(.vertical, 8)
-                        .background(Color.blue.opacity(0.2))
-                        .cornerRadius(8)
-                }
-                .frame(maxWidth: .infinity, alignment: .center)
-                .padding(.horizontal)
-            }.padding(.horizontal)
-
-            VStack(alignment: .leading, spacing: 10) {
-                Text("Already using Trio and updating from an older version?").bold()
-                BulletPoint(String(localized: "Your therapy settings, pump, and CGM configurations will be carried over."))
-                BulletPoint(
-                    String(
-                        localized: "Your algorithm settings (previously called \"OpenAPS settings\") will be reset to defaults."
-                    )
-                )
-                BulletPoint(String(localized: "We recommend reviewing them carefully — Trio will guide you step-by-step."))
-            }.padding(.horizontal)
-
-            VStack(alignment: .leading, spacing: 10) {
-                Text("One last thing, before you begin...").bold()
-                HStack {
-                    Text("You can pause at any time. Just be aware: if you ")
-                        + Text("force quit").bold()
-                        + Text(" the app before finishing onboarding, ")
-                        + Text("your progress will not be saved.").bold()
-                }
-            }.multilineTextAlignment(.leading)
-                .padding(.horizontal)
-
-            Divider()
-
-            Toggle(isOn: $state.hasReadImportantStartupNotes) {
-                Text("Got it! I'm ready to continue.").padding(.leading, 6).bold()
-            }
-            .toggleStyle(CheckboxToggleStyle(tint: Color.blue))
-            .padding(.horizontal)
-        }
-    }
-}

+ 22 - 18
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift

@@ -8,27 +8,31 @@ struct WelcomeStepView: View {
 
             Spacer(minLength: 10)
 
-            Text("Hi there!")
-                .font(.title2)
-                .fontWeight(.bold)
-                .multilineTextAlignment(.center)
+            VStack(alignment: .leading, spacing: 20) {
+                Text("Hi there!")
+                    .font(.title2)
+                    .fontWeight(.bold)
+                    .multilineTextAlignment(.center)
 
-            Text(
-                "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
-            )
-            .multilineTextAlignment(.center)
-            .foregroundColor(.secondary)
+                Text(
+                    "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
+                )
+                .multilineTextAlignment(.leading)
+                .foregroundColor(.secondary)
 
-            Text(
-                "Trio is designed to help manage your diabetes efficiently. To get the most out of the app, we'll guide you through setting up some essential parameters."
-            )
-            .multilineTextAlignment(.center)
-            .foregroundColor(.secondary)
+                Text(
+                    "Trio is designed to help manage your diabetes efficiently. To get the most out of the app, we'll guide you through setting up some essential parameters."
+                )
+                .multilineTextAlignment(.leading)
+                .foregroundColor(.secondary)
 
-            Text("Let's go through a few quick steps to ensure Trio works optimally for you.")
-                .multilineTextAlignment(.center)
-                .foregroundColor(.primary)
-                .bold()
+                Text("Let's go through a few quick steps to ensure Trio works optimally for you.")
+                    .multilineTextAlignment(.leading)
+                    .foregroundColor(.primary)
+                    .bold()
+            }
+            .padding()
+            .frame(maxWidth: .infinity, alignment: .leading)
         }
         .padding()
         .frame(maxWidth: .infinity)

+ 7 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift

@@ -68,6 +68,13 @@ extension View {
     }
 }
 
+enum AlgorithmSettingsOverviewSubstep: Int, CaseIterable, Identifiable {
+    case contents
+    case importantNotes
+
+    var id: Int { rawValue }
+}
+
 enum AlgorithmSettingsSubstep: Int, CaseIterable, Identifiable {
     case autosensMin
     case autosensMax

+ 119 - 11
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -6,10 +6,96 @@ enum OnboardingNavigationDirection {
     case backward
 }
 
+enum OnboardingChapter: Int, CaseIterable {
+    case prepareTrio
+    case therapySettings
+    case deliveryLimits
+    case algorithmSettings
+    case permissionRequests
+
+    var id: Int { rawValue }
+
+    var title: String {
+        switch self {
+        case .prepareTrio:
+            return String(localized: "Prepare Trio")
+        case .therapySettings:
+            return String(localized: "Therapy Settings")
+        case .deliveryLimits:
+            return String(localized: "Delivery Limits")
+        case .algorithmSettings:
+            return String(localized: "Algorithm Settings")
+        case .permissionRequests:
+            return String(localized: "Permission Requests")
+        }
+    }
+
+    var overviewDescription: String {
+        switch self {
+        case .prepareTrio:
+            return String(
+                localized: "Configure diagnostics sharing, optionally sync with Nightscout, and enter essentials."
+            )
+        case .therapySettings:
+            return String(
+                localized: "Define your glucose targets, basal rates, carb ratios, and insulin sensitivities."
+            )
+        case .deliveryLimits:
+            return String(
+                localized: "Set boundaries for insulin delivery and carb entries to help Trio keep you safe."
+            )
+        case .algorithmSettings:
+            return String(
+                localized: "Customize Trio’s algorithm features. Most users start with the recommended settings."
+            )
+        case .permissionRequests:
+            return String(
+                localized: "Authorize Trio to send notifications and use Bluetooth. You must allow both for Trio to work properly."
+            )
+        }
+    }
+
+    var duration: String {
+        switch self {
+        case .prepareTrio:
+            return "3-5"
+        case .therapySettings:
+            return "5-10"
+        case .deliveryLimits:
+            return "3-5"
+        case .algorithmSettings:
+            return "5-10"
+        case .permissionRequests:
+            return "1"
+        }
+    }
+
+    var completedDescription: String {
+        switch self {
+        case .prepareTrio:
+            return String(
+                localized: "App diagnostics sharing, Nightscout setup, and unit and pump model selection are all complete."
+            )
+        case .therapySettings:
+            return String(
+                localized: "Glucose target, basal rates, carb ratios, and insulin sensitivity match your needs."
+            )
+        case .deliveryLimits:
+            return String(
+                localized: "Safety boundaries for insulin delivery and carb entries are set to help Trio keep you safe."
+            )
+        case .algorithmSettings:
+            return String(localized: "Trio’s algorithm features are customized to fit your preferences and needs.")
+        case .permissionRequests:
+            return String(localized: "Notifications and Bluetooth permissions are handled to your liking.")
+        }
+    }
+}
+
 /// Represents the different steps in the onboarding process.
 enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
     case welcome
-    case startupGuide
+    case startupInfo
     case overview
     case diagnostics
     case nightscout
@@ -33,17 +119,12 @@ enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
         self == .deliveryLimits
     }
 
-    var substeps: [DeliveryLimitSubstep] {
-        guard hasSubsteps else { return [] }
-        return DeliveryLimitSubstep.allCases
-    }
-
     /// The title to display for this onboarding step.
     var title: String {
         switch self {
         case .welcome:
             return String(localized: "Welcome to Trio")
-        case .startupGuide:
+        case .startupInfo:
             return String(localized: "Startup Guide")
         case .overview:
             return String(localized: "Overview")
@@ -87,7 +168,7 @@ enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
             return String(
                 localized: "Trio is a powerful app that helps you manage your diabetes. Let's get started by setting up a few important parameters that will help Trio work effectively for you."
             )
-        case .startupGuide:
+        case .startupInfo:
             return String(
                 localized: "Trio comes with a helpful Startup Guide. We recommend opening it now and following along as you go — side by side."
             )
@@ -159,7 +240,7 @@ enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
         switch self {
         case .welcome:
             return "hand.wave.fill"
-        case .startupGuide:
+        case .startupInfo:
             return "list.bullet.clipboard.fill"
         case .overview:
             return "checklist.unchecked"
@@ -225,7 +306,7 @@ enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
              .notifications,
              .overview,
              .smbSettings,
-             .startupGuide,
+             .startupInfo,
              .targetBehavior,
              .unitSelection,
              .welcome:
@@ -240,10 +321,37 @@ enum OnboardingStep: Int, CaseIterable, Identifiable, Equatable {
             return Color.red
         }
     }
+
+    var chapterCompletion: OnboardingChapter? {
+        switch self {
+        case .unitSelection:
+            return .prepareTrio
+        case .insulinSensitivity:
+            return .therapySettings
+        case .deliveryLimits:
+            // ❗ Delivery Limits depends on the substep, not just the step.
+            // Skip here
+            return nil
+        case .targetBehavior:
+            // ❗ Target Behavior depends on the substep, not just the step.
+            // Skip here
+            return nil
+        default:
+            return nil
+        }
+    }
 }
 
 var nonInfoOnboardingSteps: [OnboardingStep] { OnboardingStep.allCases
-    .filter { $0 != .welcome && $0 != .startupGuide && $0 != .overview && $0 != .completed }
+    .filter { $0 != .welcome && $0 != .startupInfo && $0 != .overview && $0 != .completed }
+}
+
+enum StartupSubstep: Int, CaseIterable, Identifiable {
+    case startupGuide
+    case returningUser
+    case forceCloseWarning
+
+    var id: Int { rawValue }
 }
 
 enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {

+ 1 - 1
Trio/Sources/Services/Bluetooth/BluetoothStateManager.swift

@@ -9,7 +9,7 @@ public class BaseBluetoothStateManager: NSObject, BluetoothStateManager, Injecta
     private var completion: ((BluetoothAuthorization) -> Void)?
     private var centralManager: CBCentralManager?
     private var bluetoothObservers = WeakSynchronizedSet<BluetoothObserver>()
-    
+
     init(resolver: Resolver) {
         super.init()
         injectServices(resolver)

+ 5 - 1
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -497,7 +497,11 @@ extension BaseUserNotificationsManager: alertMessageNotificationObserver {
         }
         switch message.subtype {
         case .pump:
-            identifier = .pumpNotification
+            if message.type == .info || message.type == .error {
+                identifier = Identifier.alertMessageNotification
+            } else {
+                identifier = .pumpNotification
+            }
         case .carb:
             identifier = .carbsRequiredNotification
         case .glucose:

+ 82 - 0
TrioTests/JSONImporterData/carbhistory.json

@@ -0,0 +1,82 @@
+[
+  {
+    "carbs" : 10,
+    "_id" : "767AB5E3-D494-47B1-A057-084E26C4673A",
+    "fat" : 0,
+    "protein" : 0,
+    "created_at" : "2025-04-28T18:36:06.968Z",
+    "enteredBy" : "Trio",
+    "isFPU" : false,
+    "note" : "Snack 🍪"
+  },
+  {
+    "created_at" : "2025-04-28T18:10:32.027Z",
+    "_id" : "7DF8723D-F6BB-4C15-8761-1714E9269B87",
+    "note" : "",
+    "fat" : 0,
+    "isFPU" : false,
+    "protein" : 0,
+    "enteredBy" : "Trio",
+    "carbs" : 45
+  },
+  {
+    "created_at" : "2025-04-28T14:37:24.802Z",
+    "carbs" : 10,
+    "_id" : "3EB9212B-0BF6-4842-87B6-4204C28F64DD",
+    "isFPU" : false,
+    "fat" : 0,
+    "note" : "",
+    "enteredBy" : "Trio",
+    "protein" : 0
+  },
+  {
+    "protein" : 0,
+    "fat" : 0,
+    "created_at" : "2025-04-28T14:19:06.544Z",
+    "note" : "",
+    "isFPU" : false,
+    "_id" : "841383CF-BBE2-449F-B43A-7E3877B335C5",
+    "enteredBy" : "Trio",
+    "carbs" : 10
+  },
+  {
+    "_id" : "5D872E78-00D1-43B8-B38B-44DB07E3C7AD",
+    "carbs" : 20,
+    "protein" : 0,
+    "isFPU" : false,
+    "fat" : 0,
+    "note" : "",
+    "enteredBy" : "Trio",
+    "created_at" : "2025-04-28T14:04:45.343Z"
+  },
+  {
+    "note" : "",
+    "fat" : 0,
+    "created_at" : "2025-04-28T12:59:03.344Z",
+    "_id" : "637DA05B-5761-46C6-9862-84953CA99F70",
+    "isFPU" : false,
+    "protein" : 0,
+    "enteredBy" : "Trio",
+    "carbs" : 40
+  },
+  {
+    "protein" : 0,
+    "fat" : 0,
+    "note" : "",
+    "isFPU" : false,
+    "enteredBy" : "Trio",
+    "carbs" : 35,
+    "created_at" : "2025-04-28T08:38:33.763Z",
+    "_id" : "4EF0E6CA-BC02-4C02-AE28-4F719B327AAB"
+  },
+  {
+    "fat" : 0,
+    "protein" : 0,
+    "carbs" : 25,
+    "_id" : "100DAC30-7C9D-42F8-8D69-451A8660A27A",
+    "created_at" : "2025-04-28T05:03:43.332Z",
+    "note" : "",
+    "enteredBy" : "Trio",
+    "isFPU" : false
+  }
+]

+ 224 - 0
TrioTests/JSONImporterData/enacted.json

@@ -0,0 +1,224 @@
+{
+  "ISF" : 4.6,
+  "recieved" : true,
+  "reason" : "Autosens ratio: 0.99, ISF: 4.5→4.6, COB: 34, Dev: 4.2, BGI: -0.4, CR: 15, Target: 5.2, minPredBG 6.5, minGuardBG 4.3, IOBpredBG 2.7, COBpredBG 8.9, UAMpredBG 3.4, TDD: 26.95 U, 83% Bolus 17% Basal, Dynamic ISF/CR: On/Off, Sigmoid function, AF: 0.14, Basal ratio: 0.93; Eventual BG 8.9 >= 5.2,  insulinReq 0.29; setting 60m low temp of 0U/h. Microbolusing 0.1U. ",
+  "current_target" : 94,
+  "expectedDelta" : -5.9,
+  "insulinReq" : 0.29,
+  "predBGs" : {
+    "ZT" : [
+      85,
+      78,
+      71,
+      64,
+      58,
+      52,
+      47,
+      42,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      42,
+      44,
+      47,
+      49,
+      52,
+      54,
+      57,
+      59,
+      62,
+      65
+    ],
+    "IOB" : [
+      85,
+      89,
+      92,
+      95,
+      97,
+      99,
+      99,
+      99,
+      99,
+      97,
+      95,
+      92,
+      89,
+      85,
+      82,
+      79,
+      77,
+      74,
+      72,
+      70,
+      68,
+      66,
+      65,
+      63,
+      62,
+      60,
+      59,
+      58,
+      57,
+      56,
+      56,
+      55,
+      54,
+      53,
+      53,
+      52,
+      52,
+      51,
+      51,
+      51,
+      50,
+      50,
+      50,
+      50,
+      49
+    ],
+    "UAM" : [
+      85,
+      89,
+      93,
+      96,
+      99,
+      101,
+      102,
+      103,
+      104,
+      104,
+      103,
+      102,
+      100,
+      98,
+      95,
+      92,
+      89,
+      87,
+      84,
+      82,
+      80,
+      78,
+      77,
+      75,
+      74,
+      73,
+      71,
+      70,
+      69,
+      69,
+      68,
+      67,
+      66,
+      66,
+      65,
+      65,
+      64,
+      64,
+      63,
+      63,
+      63,
+      62,
+      62,
+      62,
+      61
+    ],
+    "COB" : [
+      85,
+      90,
+      94,
+      99,
+      103,
+      108,
+      112,
+      117,
+      121,
+      125,
+      130,
+      134,
+      137,
+      141,
+      145,
+      148,
+      151,
+      154,
+      157,
+      159,
+      161,
+      163,
+      165,
+      166,
+      167,
+      168,
+      168,
+      168,
+      168,
+      168,
+      167,
+      166,
+      165,
+      165,
+      164,
+      164,
+      163,
+      163,
+      162,
+      162,
+      161,
+      161,
+      161,
+      161,
+      160
+    ]
+  },
+  "reservoir" : 3735928559,
+  "IOB" : 1.249,
+  "eventualBG" : 160,
+  "units" : 0.1,
+  "TDD" : 26.95,
+  "bg" : 85,
+  "duration" : 60,
+  "deliverAt" : "2025-04-28T19:41:43.564Z",
+  "manualBolusErrorString" : 0,
+  "rate" : 0,
+  "temp" : "absolute",
+  "minDelta" : 5,
+  "COB" : 34,
+  "insulin" : {
+    "scheduled_basal" : 0.25,
+    "bolus" : 22.4,
+    "TDD" : 26.95,
+    "temp_basal" : 4.3
+  },
+  "insulinForManualBolus" : 0.8,
+  "timestamp" : "2025-04-28T19:41:48.453Z",
+  "sensitivityRatio" : 0.9863849810728643,
+  "threshold" : 3.7
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 3012 - 0
TrioTests/JSONImporterData/glucose.json


+ 173 - 0
TrioTests/JSONImporterData/newerSuggested.json

@@ -0,0 +1,173 @@
+{
+  "deliverAt" : "2025-04-28T19:51:48.453Z",
+  "insulinReq" : 0,
+  "current_target" : 178,
+  "reservoir" : 3735928559,
+  "threshold" : 6,
+  "IOB" : -0.1,
+  "ISF" : 10.5,
+  "reason" : "Autosens ratio: 0.94, ISF: 9.9→10.5, COB: 0, Dev: 0.1, BGI: 0, CR: 13, Target: 9.9, minPredBG 6.7, minGuardBG 6.2, IOBpredBG 7.3, UAMpredBG 7.2; Eventual BG 7.3 < 9.9, setting 30m zero temp.  24m left and 0 ~ req 0U/hr: no temp required",
+  "manualBolusErrorString" : 0,
+  "insulinForManualBolus" : 0,
+  "COB" : 0,
+  "TDD" : 0,
+  "bg" : 111,
+  "minDelta" : 0.5,
+  "eventualBG" : 131,
+  "recieved" : false,
+  "sensitivityRatio" : 0.94,
+  "temp" : "absolute",
+  "expectedDelta" : 2,
+  "timestamp" : "2025-04-28T19:51:48.453Z",
+  "predBGs" : {
+    "UAM" : [
+      111,
+      111,
+      111,
+      111,
+      112,
+      112,
+      113,
+      113,
+      113,
+      114,
+      114,
+      115,
+      115,
+      116,
+      116,
+      117,
+      118,
+      118,
+      119,
+      119,
+      120,
+      120,
+      121,
+      121,
+      122,
+      122,
+      123,
+      123,
+      123,
+      124,
+      124,
+      125,
+      125,
+      125,
+      126,
+      126,
+      126,
+      126,
+      127,
+      127,
+      127,
+      128,
+      128,
+      128,
+      128,
+      128,
+      128,
+      129
+    ],
+    "IOB" : [
+      111,
+      111,
+      112,
+      112,
+      113,
+      114,
+      114,
+      115,
+      115,
+      116,
+      116,
+      117,
+      118,
+      118,
+      119,
+      119,
+      120,
+      120,
+      121,
+      122,
+      122,
+      122,
+      123,
+      123,
+      124,
+      124,
+      125,
+      125,
+      126,
+      126,
+      126,
+      127,
+      127,
+      128,
+      128,
+      128,
+      128,
+      129,
+      129,
+      129,
+      130,
+      130,
+      130,
+      130,
+      130,
+      130,
+      131
+    ],
+    "ZT" : [
+      111,
+      111,
+      111,
+      112,
+      112,
+      113,
+      114,
+      115,
+      117,
+      119,
+      121,
+      124,
+      127,
+      131,
+      134,
+      138,
+      143,
+      148,
+      153,
+      158,
+      164,
+      171,
+      177,
+      184,
+      191,
+      199,
+      206,
+      214,
+      223,
+      231,
+      240,
+      249,
+      259,
+      268,
+      278,
+      288,
+      298,
+      308,
+      319,
+      329,
+      340,
+      351,
+      362,
+      374,
+      385,
+      397,
+      401,
+      401
+    ]
+  }
+}

+ 557 - 0
TrioTests/JSONImporterData/pumphistory-24h-zoned.json

@@ -0,0 +1,557 @@
+[
+  {
+    "isExternalInsulin" : false,
+    "_type" : "Bolus",
+    "duration" : 0,
+    "id" : "d481de63c48f9009cb16d5e73686e001",
+    "amount" : 0.3,
+    "timestamp" : "2025-04-29T01:33:57.369Z",
+    "isSMB" : true
+  },
+  {
+    "duration (min)" : 30,
+    "id" : "7c93508c15eb34128bade375a8256275",
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-29T01:33:57.348Z"
+  },
+  {
+    "_type" : "TempBasal",
+    "rate" : 2,
+    "timestamp" : "2025-04-29T01:33:57.348Z",
+    "id" : "_7c93508c15eb34128bade375a8256275",
+    "temp" : "absolute"
+  },
+  {
+    "timestamp" : "2025-04-29T01:31:29.265Z",
+    "_type" : "PumpResume",
+    "id" : "80391397e3634568f628d239e7e8a658"
+  },
+  {
+    "id" : "cc8c69dd0f88f5f1737ecc9c9decd476",
+    "_type" : "PumpSuspend",
+    "timestamp" : "2025-04-29T01:31:12.348Z"
+  },
+  {
+    "timestamp" : "2025-04-29T01:29:32.123Z",
+    "amount" : 0.4,
+    "id" : "e327b654c3d8ae1a88ac6349b3c66168",
+    "duration" : 0,
+    "isSMB" : true,
+    "_type" : "Bolus",
+    "isExternalInsulin" : false
+  },
+  {
+    "duration (min)" : 30,
+    "timestamp" : "2025-04-29T01:29:32.107Z",
+    "id" : "d2d0907111c4fd7feacc303e22b29808",
+    "_type" : "TempBasalDuration"
+  },
+  {
+    "rate" : 2,
+    "_type" : "TempBasal",
+    "id" : "_d2d0907111c4fd7feacc303e22b29808",
+    "timestamp" : "2025-04-29T01:29:32.107Z",
+    "temp" : "absolute"
+  },
+  {
+    "duration" : 0,
+    "amount" : 0.2,
+    "id" : "7ad260001c2030a67edf39048d6758a4",
+    "isExternalInsulin" : false,
+    "isSMB" : true,
+    "_type" : "Bolus",
+    "timestamp" : "2025-04-28T19:35:25.796Z"
+  },
+  {
+    "duration (min)" : 30,
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T19:35:25.777Z",
+    "id" : "21069fce8cc588a25ec81e18504c0a59"
+  },
+  {
+    "rate" : 0,
+    "_type" : "TempBasal",
+    "id" : "_21069fce8cc588a25ec81e18504c0a59",
+    "temp" : "absolute",
+    "timestamp" : "2025-04-28T19:35:25.777Z"
+  },
+  {
+    "id" : "7f953a8db58792d6feac375435b5c517",
+    "timestamp" : "2025-04-28T19:28:23.023Z",
+    "_type" : "Bolus",
+    "duration" : 0,
+    "isExternalInsulin" : false,
+    "isSMB" : true,
+    "amount" : 0.4
+  },
+  {
+    "id" : "fba678c82ede44e3c29a3249943832ea",
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T19:28:23.004Z",
+    "duration (min)" : 30
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_fba678c82ede44e3c29a3249943832ea",
+    "timestamp" : "2025-04-28T19:28:23.004Z",
+    "temp" : "absolute",
+    "rate" : 0
+  },
+  {
+    "amount" : 0.2,
+    "timestamp" : "2025-04-28T19:23:23.016Z",
+    "id" : "2b94ab0cdc9cb5a9f0bd2daca999587a",
+    "duration" : 0,
+    "isSMB" : true,
+    "_type" : "Bolus",
+    "isExternalInsulin" : false
+  },
+  {
+    "timestamp" : "2025-04-28T19:23:22.998Z",
+    "duration (min)" : 30,
+    "id" : "6bf57ed918f46661eac3bb4dc319faed",
+    "_type" : "TempBasalDuration"
+  },
+  {
+    "temp" : "absolute",
+    "_type" : "TempBasal",
+    "id" : "_6bf57ed918f46661eac3bb4dc319faed",
+    "timestamp" : "2025-04-28T19:23:22.998Z",
+    "rate" : 0
+  },
+  {
+    "isSMB" : true,
+    "duration" : 0,
+    "isExternalInsulin" : false,
+    "_type" : "Bolus",
+    "id" : "3d7cecc1580b7b4681bbbd94a128cd87",
+    "amount" : 0.3,
+    "timestamp" : "2025-04-28T19:18:23.062Z"
+  },
+  {
+    "timestamp" : "2025-04-28T19:18:23.045Z",
+    "_type" : "TempBasalDuration",
+    "id" : "a271d613cbab94ec62fcc0ff61ec00b1",
+    "duration (min)" : 30
+  },
+  {
+    "id" : "_a271d613cbab94ec62fcc0ff61ec00b1",
+    "rate" : 0,
+    "_type" : "TempBasal",
+    "temp" : "absolute",
+    "timestamp" : "2025-04-28T19:18:23.045Z"
+  },
+  {
+    "id" : "e902a3dea7187f2e2a7a0c0ba4ad352c",
+    "amount" : 0.3,
+    "timestamp" : "2025-04-28T19:13:23.014Z",
+    "isSMB" : true,
+    "isExternalInsulin" : false,
+    "_type" : "Bolus",
+    "duration" : 0
+  },
+  {
+    "_type" : "TempBasalDuration",
+    "duration (min)" : 30,
+    "timestamp" : "2025-04-28T19:13:22.995Z",
+    "id" : "7d4c0396a72c6da46f816507a84e4e1e"
+  },
+  {
+    "timestamp" : "2025-04-28T19:13:22.995Z",
+    "temp" : "absolute",
+    "rate" : 0.9,
+    "_type" : "TempBasal",
+    "id" : "_7d4c0396a72c6da46f816507a84e4e1e"
+  },
+  {
+    "timestamp" : "2025-04-28T19:08:22.991Z",
+    "_type" : "Bolus",
+    "duration" : 0,
+    "isSMB" : true,
+    "amount" : 0.3,
+    "id" : "8ed3c7ebd15062b6051310dbf5df1a03",
+    "isExternalInsulin" : false
+  },
+  {
+    "id" : "0dd2971d37387b7473440c38ab7ecf82",
+    "duration (min)" : 30,
+    "timestamp" : "2025-04-28T19:08:22.974Z",
+    "_type" : "TempBasalDuration"
+  },
+  {
+    "id" : "_0dd2971d37387b7473440c38ab7ecf82",
+    "_type" : "TempBasal",
+    "rate" : 0.83,
+    "temp" : "absolute",
+    "timestamp" : "2025-04-28T19:08:22.974Z"
+  },
+  {
+    "duration" : 0,
+    "isExternalInsulin" : false,
+    "isSMB" : true,
+    "timestamp" : "2025-04-28T19:03:23.006Z",
+    "id" : "2fc663c512044268096d2a3cdfded146",
+    "_type" : "Bolus",
+    "amount" : 0.1
+  },
+  {
+    "id" : "baa5ecc3dd4ff8af8746ed3423483fcc",
+    "timestamp" : "2025-04-28T19:03:22.990Z",
+    "duration (min)" : 30,
+    "_type" : "TempBasalDuration"
+  },
+  {
+    "timestamp" : "2025-04-28T19:03:22.990Z",
+    "rate" : 0.93,
+    "_type" : "TempBasal",
+    "temp" : "absolute",
+    "id" : "_baa5ecc3dd4ff8af8746ed3423483fcc"
+  },
+  {
+    "duration" : 0,
+    "isSMB" : true,
+    "timestamp" : "2025-04-28T18:53:23.003Z",
+    "_type" : "Bolus",
+    "isExternalInsulin" : false,
+    "amount" : 0.1,
+    "id" : "2b55e3317e35d5a24867bd89ec796ad7"
+  },
+  {
+    "id" : "8d225604b716f1a96a990c3cc856e0dd",
+    "timestamp" : "2025-04-28T18:53:22.980Z",
+    "_type" : "TempBasalDuration",
+    "duration (min)" : 30
+  },
+  {
+    "rate" : 0,
+    "temp" : "absolute",
+    "id" : "_8d225604b716f1a96a990c3cc856e0dd",
+    "_type" : "TempBasal",
+    "timestamp" : "2025-04-28T18:53:22.980Z"
+  },
+  {
+    "id" : "4d6681b9a8052f38dabe30ea4311451d",
+    "timestamp" : "2025-04-28T18:18:22.962Z",
+    "duration (min)" : 90,
+    "_type" : "TempBasalDuration"
+  },
+  {
+    "temp" : "absolute",
+    "rate" : 0,
+    "_type" : "TempBasal",
+    "id" : "_4d6681b9a8052f38dabe30ea4311451d",
+    "timestamp" : "2025-04-28T18:18:22.962Z"
+  },
+  {
+    "id" : "960ddedcaa1dec242596f651e72538b4",
+    "duration (min)" : 30,
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T18:12:23.531Z"
+  },
+  {
+    "temp" : "absolute",
+    "id" : "_960ddedcaa1dec242596f651e72538b4",
+    "_type" : "TempBasal",
+    "timestamp" : "2025-04-28T18:12:23.531Z",
+    "rate" : 0
+  },
+  {
+    "isExternalInsulin" : false,
+    "amount" : 0.2,
+    "duration" : 0,
+    "isSMB" : true,
+    "id" : "4e5853d7e76826a3532f1057e90c30fc",
+    "timestamp" : "2025-04-28T17:19:40.746Z",
+    "_type" : "Bolus"
+  },
+  {
+    "timestamp" : "2025-04-28T17:19:39.319Z",
+    "_type" : "TempBasalDuration",
+    "id" : "5d77132435da87c6c6f09ab463b76110",
+    "duration (min)" : 60
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_5d77132435da87c6c6f09ab463b76110",
+    "temp" : "absolute",
+    "rate" : 0,
+    "timestamp" : "2025-04-28T17:19:39.319Z"
+  },
+  {
+    "timestamp" : "2025-04-28T17:14:53.896Z",
+    "id" : "ad1bd46679617f6a3f0d85b802d2bba1",
+    "isExternalInsulin" : false,
+    "_type" : "Bolus",
+    "amount" : 0.1,
+    "duration" : 0,
+    "isSMB" : true
+  },
+  {
+    "timestamp" : "2025-04-28T17:14:50.272Z",
+    "duration (min)" : 60,
+    "_type" : "TempBasalDuration",
+    "id" : "5347925783661996be2c5c9b995f2bdd"
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_5347925783661996be2c5c9b995f2bdd",
+    "rate" : 0,
+    "temp" : "absolute",
+    "timestamp" : "2025-04-28T17:14:50.272Z"
+  },
+  {
+    "id" : "c3a4cb6b08f3656e400d007f079feeaf",
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T16:58:56.627Z",
+    "duration (min)" : 30
+  },
+  {
+    "timestamp" : "2025-04-28T16:58:56.627Z",
+    "temp" : "absolute",
+    "_type" : "TempBasal",
+    "rate" : 1,
+    "id" : "_c3a4cb6b08f3656e400d007f079feeaf"
+  },
+  {
+    "id" : "d924c295dcbcc397d1434ac7605c0470",
+    "timestamp" : "2025-04-28T16:54:58.495Z",
+    "isSMB" : false,
+    "duration" : 1,
+    "amount" : 2.9,
+    "isExternalInsulin" : false,
+    "_type" : "Bolus"
+  },
+  {
+    "isSMB" : false,
+    "id" : "797a50e3cff4d3f8b5776f3d6ff990d7",
+    "isExternalInsulin" : false,
+    "duration" : 0,
+    "amount" : 0.3,
+    "timestamp" : "2025-04-28T16:53:17.358Z",
+    "_type" : "Bolus"
+  },
+  {
+    "amount" : 0.1,
+    "isExternalInsulin" : false,
+    "timestamp" : "2025-04-28T16:53:03.111Z",
+    "_type" : "Bolus",
+    "id" : "8bc5da18065649d6e4809274d027574b",
+    "isSMB" : true,
+    "duration" : 0
+  },
+  {
+    "id" : "62bd97a9089450706d50923b94858f9e",
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T16:53:03.098Z",
+    "duration (min)" : 30
+  },
+  {
+    "rate" : 0.07,
+    "timestamp" : "2025-04-28T16:53:03.098Z",
+    "_type" : "TempBasal",
+    "temp" : "absolute",
+    "id" : "_62bd97a9089450706d50923b94858f9e"
+  },
+  {
+    "isExternalInsulin" : false,
+    "timestamp" : "2025-04-28T15:53:23.447Z",
+    "_type" : "Bolus",
+    "amount" : 0.3,
+    "duration" : 0,
+    "id" : "07c77569f169edf0e68a67c20fb9ae98",
+    "isSMB" : true
+  },
+  {
+    "isExternalInsulin" : false,
+    "amount" : 0.5,
+    "id" : "6260c29e81241f14626cab992ba5631d",
+    "isSMB" : true,
+    "duration" : 0,
+    "timestamp" : "2025-04-28T15:48:23.476Z",
+    "_type" : "Bolus"
+  },
+  {
+    "duration (min)" : 30,
+    "timestamp" : "2025-04-28T15:48:23.459Z",
+    "id" : "f5bfe14b1c2e1f3526d30a7deee2099e",
+    "_type" : "TempBasalDuration"
+  },
+  {
+    "timestamp" : "2025-04-28T15:48:23.459Z",
+    "rate" : 2,
+    "temp" : "absolute",
+    "id" : "_f5bfe14b1c2e1f3526d30a7deee2099e",
+    "_type" : "TempBasal"
+  },
+  {
+    "duration" : 0,
+    "timestamp" : "2025-04-28T15:43:23.485Z",
+    "id" : "bc08ebe9e0734e35aa530d6ed67099c9",
+    "isSMB" : true,
+    "_type" : "Bolus",
+    "amount" : 0.1,
+    "isExternalInsulin" : false
+  },
+  {
+    "duration (min)" : 30,
+    "timestamp" : "2025-04-28T15:43:23.458Z",
+    "_type" : "TempBasalDuration",
+    "id" : "7e881182f96b998ae7497ae0704dcb22"
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_7e881182f96b998ae7497ae0704dcb22",
+    "rate" : 1.65,
+    "timestamp" : "2025-04-28T15:43:23.458Z",
+    "temp" : "absolute"
+  },
+  {
+    "id" : "338b54233a83febb08ac177360cfd406",
+    "duration (min)" : 30,
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T15:38:23.432Z"
+  },
+  {
+    "id" : "_338b54233a83febb08ac177360cfd406",
+    "temp" : "absolute",
+    "_type" : "TempBasal",
+    "timestamp" : "2025-04-28T15:38:23.432Z",
+    "rate" : 1.05
+  },
+  {
+    "_type" : "Bolus",
+    "timestamp" : "2025-04-28T15:33:23.425Z",
+    "id" : "340a2947432107fa513e55b334017a03",
+    "amount" : 0.1,
+    "isExternalInsulin" : false,
+    "duration" : 0,
+    "isSMB" : true
+  },
+  {
+    "_type" : "Bolus",
+    "isSMB" : true,
+    "timestamp" : "2025-04-28T15:29:23.433Z",
+    "id" : "c9a5a3d82a73a1b5667b8eab3d60ce45",
+    "isExternalInsulin" : false,
+    "amount" : 0.2,
+    "duration" : 0
+  },
+  {
+    "duration" : 0,
+    "isExternalInsulin" : false,
+    "timestamp" : "2025-04-28T15:17:23.965Z",
+    "id" : "918c040eb7d3de7eeae0fadee0454599",
+    "amount" : 0.5,
+    "_type" : "Bolus",
+    "isSMB" : true
+  },
+  {
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T15:17:23.951Z",
+    "id" : "517675d88140e1ac4728a87e98600f3f",
+    "duration (min)" : 30
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_517675d88140e1ac4728a87e98600f3f",
+    "timestamp" : "2025-04-28T15:17:23.951Z",
+    "rate" : 2,
+    "temp" : "absolute"
+  },
+  {
+    "amount" : 0.1,
+    "id" : "9c97aba493f7e042e2efc11b716d409a",
+    "timestamp" : "2025-04-28T14:47:51.215Z",
+    "duration" : 0,
+    "_type" : "Bolus",
+    "isSMB" : true,
+    "isExternalInsulin" : false
+  },
+  {
+    "id" : "28410f9ace07451084d4f656d795825a",
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T14:47:51.198Z",
+    "duration (min)" : 30
+  },
+  {
+    "_type" : "TempBasal",
+    "rate" : 1.3,
+    "timestamp" : "2025-04-28T14:47:51.198Z",
+    "id" : "_28410f9ace07451084d4f656d795825a",
+    "temp" : "absolute"
+  },
+  {
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T12:38:37.849Z",
+    "id" : "27e680025553869b98177308409faf17",
+    "duration (min)" : 30
+  },
+  {
+    "_type" : "TempBasal",
+    "timestamp" : "2025-04-28T12:38:37.849Z",
+    "id" : "_27e680025553869b98177308409faf17",
+    "temp" : "absolute",
+    "rate" : 1
+  },
+  {
+    "_type" : "TempBasalDuration",
+    "id" : "4d5bcf121b1a7ed71ba48654c5479cd6",
+    "timestamp" : "2025-04-28T12:33:37.831Z",
+    "duration (min)" : 30
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_4d5bcf121b1a7ed71ba48654c5479cd6",
+    "rate" : 0.8,
+    "timestamp" : "2025-04-28T12:33:37.831Z",
+    "temp" : "absolute"
+  },
+  {
+    "_type" : "TempBasalDuration",
+    "id" : "89cae79781bd2c1ab281371dba13a5f0",
+    "timestamp" : "2025-04-28T12:23:38.200Z",
+    "duration (min)" : 30
+  },
+  {
+    "_type" : "TempBasal",
+    "id" : "_89cae79781bd2c1ab281371dba13a5f0",
+    "temp" : "absolute",
+    "timestamp" : "2025-04-28T12:23:38.200Z",
+    "rate" : 0.9
+  },
+  {
+    "duration (min)" : 30,
+    "timestamp" : "2025-04-28T12:18:37.798Z",
+    "_type" : "TempBasalDuration",
+    "id" : "3d6613118c4dcf7696eaaf36716a7ed7"
+  },
+  {
+    "temp" : "absolute",
+    "id" : "_3d6613118c4dcf7696eaaf36716a7ed7",
+    "timestamp" : "2025-04-28T12:18:37.798Z",
+    "_type" : "TempBasal",
+    "rate" : 0.45
+  },
+  {
+    "isSMB" : true,
+    "timestamp" : "2025-04-28T12:05:39.984Z",
+    "isExternalInsulin" : false,
+    "amount" : 0.1,
+    "_type" : "Bolus",
+    "duration" : 0,
+    "id" : "023ae6b2bc6cbe26fb850a3bec511f06"
+  },
+  {
+    "_type" : "TempBasalDuration",
+    "timestamp" : "2025-04-28T12:05:39.956Z",
+    "id" : "c0d06107ce05f35a483afc76f35408d2",
+    "duration (min)" : 30
+  },
+  {
+    "rate" : 1.2,
+    "id" : "_c0d06107ce05f35a483afc76f35408d2",
+    "timestamp" : "2025-04-28T12:05:39.956Z",
+    "temp" : "absolute",
+    "_type" : "TempBasal"
+  }
+]

+ 223 - 0
TrioTests/JSONImporterData/suggested.json

@@ -0,0 +1,223 @@
+{
+  "sensitivityRatio" : 0.9863849810728643,
+  "timestamp" : "2025-04-28T19:41:43.564Z",
+  "COB" : 34,
+  "IOB" : 1.249,
+  "reason" : "Autosens ratio: 0.99, ISF: 4.5→4.6, COB: 34, Dev: 4.2, BGI: -0.4, CR: 15, Target: 5.2, minPredBG 6.5, minGuardBG 4.3, IOBpredBG 2.7, COBpredBG 8.9, UAMpredBG 3.4, TDD: 26.95 U, 83% Bolus 17% Basal, Dynamic ISF/CR: On/Off, Sigmoid function, AF: 0.14, Basal ratio: 0.93; Eventual BG 8.9 >= 5.2,  insulinReq 0.29; setting 60m low temp of 0U/h. Microbolusing 0.1U. ",
+  "eventualBG" : 160,
+  "reservoir" : 3735928559,
+  "insulinReq" : 0.29,
+  "TDD" : 26.95,
+  "insulin" : {
+    "temp_basal" : 4.3,
+    "scheduled_basal" : 0.25,
+    "TDD" : 26.95,
+    "bolus" : 22.4
+  },
+  "predBGs" : {
+    "IOB" : [
+      85,
+      89,
+      92,
+      95,
+      97,
+      99,
+      99,
+      99,
+      99,
+      97,
+      95,
+      92,
+      89,
+      85,
+      82,
+      79,
+      77,
+      74,
+      72,
+      70,
+      68,
+      66,
+      65,
+      63,
+      62,
+      60,
+      59,
+      58,
+      57,
+      56,
+      56,
+      55,
+      54,
+      53,
+      53,
+      52,
+      52,
+      51,
+      51,
+      51,
+      50,
+      50,
+      50,
+      50,
+      49
+    ],
+    "ZT" : [
+      85,
+      78,
+      71,
+      64,
+      58,
+      52,
+      47,
+      42,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      39,
+      42,
+      44,
+      47,
+      49,
+      52,
+      54,
+      57,
+      59,
+      62,
+      65
+    ],
+    "UAM" : [
+      85,
+      89,
+      93,
+      96,
+      99,
+      101,
+      102,
+      103,
+      104,
+      104,
+      103,
+      102,
+      100,
+      98,
+      95,
+      92,
+      89,
+      87,
+      84,
+      82,
+      80,
+      78,
+      77,
+      75,
+      74,
+      73,
+      71,
+      70,
+      69,
+      69,
+      68,
+      67,
+      66,
+      66,
+      65,
+      65,
+      64,
+      64,
+      63,
+      63,
+      63,
+      62,
+      62,
+      62,
+      61
+    ],
+    "COB" : [
+      85,
+      90,
+      94,
+      99,
+      103,
+      108,
+      112,
+      117,
+      121,
+      125,
+      130,
+      134,
+      137,
+      141,
+      145,
+      148,
+      151,
+      154,
+      157,
+      159,
+      161,
+      163,
+      165,
+      166,
+      167,
+      168,
+      168,
+      168,
+      168,
+      168,
+      167,
+      166,
+      165,
+      165,
+      164,
+      164,
+      163,
+      163,
+      162,
+      162,
+      161,
+      161,
+      161,
+      161,
+      160
+    ]
+  },
+  "ISF" : 4.6,
+  "rate" : 0,
+  "minDelta" : 5,
+  "expectedDelta" : -5.9,
+  "threshold" : 3.7,
+  "insulinForManualBolus" : 0.8,
+  "duration" : 60,
+  "temp" : "absolute",
+  "manualBolusErrorString" : 0,
+  "deliverAt" : "2025-04-28T19:41:43.564Z",
+  "current_target" : 94,
+  "units" : 0.1,
+  "bg" : 85
+}

+ 395 - 0
TrioTests/JSONImporterTests.swift

@@ -0,0 +1,395 @@
+//
+//  JSONImporterTests.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 21.04.25.
+//
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+class BundleReference {}
+
+@Suite("JSON Importer Tests", .serialized) struct JSONImporterTests: Injectable {
+    var coreDataStack: CoreDataStack!
+    var context: NSManagedObjectContext!
+    var importer: JSONImporter!
+
+    init() async throws {
+        // In-memory Core Data for tests
+        coreDataStack = try await CoreDataStack.createForTests()
+        context = coreDataStack.newTaskContext()
+        importer = JSONImporter(context: context, coreDataStack: coreDataStack)
+    }
+
+    @Test("Import glucose history with value checks") func testImportGlucoseHistoryDetails() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "glucose", ofType: "json")!
+        let url = URL(filePath: path)
+
+        let now = Date("2025-04-28T19:32:52.000Z")!
+        try await importer.importGlucoseHistory(url: url, now: now)
+        // run the import againt to check our deduplication logic
+        try await importer.importGlucoseHistory(url: url, now: now)
+
+        let allReadings = try await coreDataStack.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored] ?? []
+
+        #expect(allReadings.count == 274)
+        #expect(allReadings.first?.glucose == 115)
+        #expect(allReadings.first?.date == Date("2025-04-28T19:32:51.727Z"))
+        #expect(allReadings.last?.glucose == 127)
+        #expect(allReadings.last?.date == Date("2025-04-27T19:37:50.327Z"))
+
+        let manualCount = allReadings.filter({ $0.isManual }).count
+        #expect(manualCount == 1)
+    }
+
+    @Test("Skip importing old glucose values") func testSkipImportOldGlucoseValues() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "glucose", ofType: "json")!
+        let url = URL(filePath: path)
+
+        // more than 24 hours in the future from the most recent entry
+        let now = Date("2025-04-29T19:32:52.000Z")!
+        try await importer.importGlucoseHistory(url: url, now: now)
+
+        let allReadings = try await coreDataStack.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored] ?? []
+
+        #expect(allReadings.isEmpty)
+    }
+
+    @Test("Import pump history with value checks") func testImportPumpHistoryDetails() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "pumphistory-24h-zoned", ofType: "json")!
+        let url = URL(filePath: path)
+
+        let now = Date("2025-04-29T01:33:58.000Z")!
+        try await importer.importPumpHistory(url: url, now: now)
+        // test out deduplication logic
+        try await importer.importPumpHistory(url: url, now: now)
+
+        let allReadings = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "timestamp",
+            ascending: false
+        ) as? [PumpEventStored] ?? []
+
+        let objectIds = allReadings.map(\.objectID)
+        let parsedHistory = OpenAPS.loadAndMapPumpEvents(objectIds, from: context)
+
+        var bolusTotal = 0.0
+        var bolusCount = 0
+        var smbCount = 0
+        var rateTotal = 0.0
+        var tempBasalCount = 0
+        var durationTotal = 0
+        var suspendCount = 0
+        var resumeCount = 0
+        for event in parsedHistory {
+            switch event {
+            case let .bolus(bolus):
+                bolusTotal += bolus.amount
+                bolusCount += 1
+                if bolus.isSMB {
+                    smbCount += 1
+                }
+            case let .tempBasal(tempBasal):
+                rateTotal += tempBasal.rate
+                tempBasalCount += 1
+            case let .tempBasalDuration(tempBasalDuration):
+                durationTotal += tempBasalDuration.duration
+            case .suspend:
+                suspendCount += 1
+            case .resume:
+                resumeCount += 1
+            default:
+                fatalError("unhandled pump event")
+            }
+        }
+
+        // see the scripts/pump-history-stats.py file for where these come from
+        #expect(parsedHistory.count == 77)
+        #expect(bolusCount == 23)
+        #expect(smbCount == 21)
+        #expect(bolusTotal.isApproximatelyEqual(to: 8.1, epsilon: 0.01))
+        #expect(tempBasalCount == 26)
+        #expect(rateTotal.isApproximatelyEqual(to: 20.08, epsilon: 0.001))
+        #expect(durationTotal == 900)
+        #expect(suspendCount == 1)
+        #expect(resumeCount == 1)
+    }
+
+    @Test("Skipping old pump history entries") func testSkipOldPumpHistoryEntries() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "pumphistory-24h-zoned", ofType: "json")!
+        let url = URL(filePath: path)
+
+        let now = Date("2025-04-30T01:33:58.000Z")!
+        try await importer.importPumpHistory(url: url, now: now)
+
+        let allReadings = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "timestamp",
+            ascending: false
+        ) as? [PumpEventStored] ?? []
+
+        #expect(allReadings.isEmpty)
+    }
+
+    @Test("Import carb history with value checks") func testImportCarbHistoryDetails() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "carbhistory", ofType: "json")!
+        let url = URL(filePath: path)
+
+        let now = Date("2025-04-28T19:32:52.000Z")!
+        try await importer.importCarbHistory(url: url, now: now)
+        // run the import againt to check our deduplication logic
+        try await importer.importCarbHistory(url: url, now: now)
+
+        let allCarbEntries = try await coreDataStack.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "date",
+            ascending: false
+        ) as? [CarbEntryStored] ?? []
+
+        #expect(allCarbEntries.count == 8)
+        #expect(allCarbEntries.first?.carbs == 10)
+        #expect(allCarbEntries.first?.note == "Snack 🍪")
+        #expect(allCarbEntries.first?.date == Date("2025-04-28T18:36:06.968Z"))
+        #expect(allCarbEntries.last?.carbs == 25)
+        #expect(allCarbEntries.last?.date == Date("2025-04-28T05:03:43.332Z"))
+    }
+
+    @Test("Skip importing old carb entries") func testSkipImportOldCarbEntries() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let path = testBundle.path(forResource: "carbhistory", ofType: "json")!
+        let url = URL(filePath: path)
+
+        // more than 24 hours in the future from the most recent entry
+        let now = Date("2025-04-29T19:32:52.000Z")!
+        try await importer.importCarbHistory(url: url, now: now)
+
+        let allCarbEntries = try await coreDataStack.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "date",
+            ascending: false
+        ) as? [CarbEntryStored] ?? []
+
+        #expect(allCarbEntries.isEmpty)
+    }
+
+    @Test("Import determination data with value checks") func testImportDeterminationDetails() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let enactedPath = testBundle.path(forResource: "enacted", ofType: "json")!
+        let enactedUrl = URL(filePath: enactedPath)
+        let suggestedPath = testBundle.path(forResource: "suggested", ofType: "json")!
+        let suggestedUrl = URL(filePath: suggestedPath)
+
+        let now = Date("2025-04-28T20:50:00.000Z")!
+        try await importer.importOrefDetermination(enactedUrl: enactedUrl, suggestedUrl: suggestedUrl, now: now)
+        // run the import againt to check our deduplication logic
+        try await importer.importOrefDetermination(enactedUrl: enactedUrl, suggestedUrl: suggestedUrl, now: now)
+
+        let determinations = try await coreDataStack.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "deliverAt",
+            ascending: false
+        ) as? [OrefDetermination] ?? []
+
+        #expect(determinations.count == 1) // single determination, as enacted.deliverAt and suggested.deliverAt match
+
+        let determination = determinations.first!
+
+        #expect(determination.deliverAt == Date("2025-04-28T19:41:43.564Z"))
+        #expect(determination.timestamp == Date("2025-04-28T19:41:48.453Z"))
+        #expect(determination.enacted == true)
+        #expect(determination.reason?.starts(with: "Autosens ratio: 0.99") == true)
+        #expect(determination.insulinReq == Decimal(string: "0.29").map(NSDecimalNumber.init))
+        #expect(determination.eventualBG! == NSDecimalNumber(160))
+        #expect(determination.sensitivityRatio == Decimal(string: "0.9863849810728643").map(NSDecimalNumber.init))
+        #expect(determination.rate == Decimal(string: "0").map(NSDecimalNumber.init))
+        #expect(determination.duration == NSDecimalNumber(60))
+        #expect(determination.iob == Decimal(string: "1.249").map(NSDecimalNumber.init))
+        #expect(determination.cob == 34)
+        #expect(determination.temp == "absolute")
+        #expect(determination.glucose == NSDecimalNumber(85))
+        #expect(determination.reservoir == Decimal(string: "3735928559").map(NSDecimalNumber.init))
+        #expect(determination.insulinSensitivity == Decimal(string: "4.6").map(NSDecimalNumber.init))
+        #expect(determination.currentTarget == Decimal(string: "94").map(NSDecimalNumber.init))
+        #expect(determination.insulinForManualBolus == Decimal(string: "0.8").map(NSDecimalNumber.init))
+        #expect(determination.manualBolusErrorString == Decimal(string: "0").map(NSDecimalNumber.init))
+        #expect(determination.minDelta == NSDecimalNumber(5))
+        #expect(determination.expectedDelta == Decimal(string: "-5.9").map(NSDecimalNumber.init))
+        #expect(determination.threshold == Decimal(string: "3.7").map(NSDecimalNumber.init))
+        #expect(determination.carbRatio == nil) // not present in JSON
+
+        let forecasts = try await coreDataStack.fetchEntitiesAsync(
+            ofType: Forecast.self,
+            onContext: context,
+            predicate: NSPredicate(format: "orefDetermination = %@", determination.objectID),
+            key: "type",
+            ascending: true,
+            relationshipKeyPathsForPrefetching: ["forecastValues"]
+        )
+
+        var forecastHierarchy: [(forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+        await context.perform {
+            if let forecasts = forecasts as? [Forecast] {
+                for forecast in forecasts {
+                    // Use the helper property that already sorts by index
+                    let sortedValues = forecast.forecastValuesArray
+                    forecastHierarchy.append((
+                        forecastID: forecast.objectID,
+                        forecastValueIDs: sortedValues.map(\.objectID)
+                    ))
+                }
+            }
+
+            for entry in forecastHierarchy {
+                var forecastValueTuple: (Forecast?, [ForecastValue]) = (nil, [])
+
+                var forecast: Forecast?
+                var forecastValues: [ForecastValue] = []
+
+                do {
+                    // Fetch the forecast object
+                    forecast = try context.existingObject(with: entry.forecastID) as? Forecast
+
+                    // Fetch the first 3h of forecast values
+                    for forecastValueID in entry.forecastValueIDs.prefix(36) {
+                        if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
+                            forecastValues.append(forecastValue)
+                        }
+                    }
+                } catch {
+                    debugPrint(
+                        "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
+                    )
+                }
+                forecastValueTuple = (forecast, forecastValues)
+
+                // Basic checks
+                #expect(forecastValueTuple.0 != nil)
+                #expect(forecastValueTuple.1.isNotEmpty == true)
+
+                if let forecast = forecastValueTuple.0 {
+                    let sortedValues = forecastValueTuple.1.sorted { $0.index < $1.index }
+                    let prefix = sortedValues.prefix(5).compactMap(\.value)
+                    let type = forecast.type?.lowercased()
+
+                    switch type {
+                    case "zt":
+                        #expect(prefix == [85, 78, 71, 64, 58])
+                    case "iob":
+                        #expect(prefix == [85, 89, 92, 95, 97])
+                    case "uam":
+                        #expect(prefix == [85, 89, 93, 96, 99])
+                    case "cob":
+                        #expect(prefix == [85, 90, 94, 99, 103])
+                    default:
+                        break // Skip unknown forecast types silently
+                    }
+                }
+            }
+        }
+    }
+
+    @Test("Skip importing old determinations") func testSkipImportOldDeterminationData() async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let enactedPath = testBundle.path(forResource: "enacted", ofType: "json")!
+        let enactedUrl = URL(filePath: enactedPath)
+        let suggestedPath = testBundle.path(forResource: "suggested", ofType: "json")!
+        let suggestedUrl = URL(filePath: suggestedPath)
+
+        // more than 24 hours in the future from the most recent entry
+        let now = Date("2025-04-29T22:00:00.000Z")!
+
+        try await importer.importOrefDetermination(enactedUrl: enactedUrl, suggestedUrl: suggestedUrl, now: now)
+
+        let determinations = try await coreDataStack.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "deliverAt",
+            ascending: false
+        ) as? [OrefDetermination] ?? []
+
+        #expect(determinations.isEmpty)
+    }
+
+    @Test("Import determination data with suggested newer than enacted") func testImportDeterminationDetailsWithNewerSuggested(
+    ) async throws {
+        let testBundle = Bundle(for: BundleReference.self)
+        let enactedPath = testBundle.path(forResource: "enacted", ofType: "json")!
+        let enactedUrl = URL(filePath: enactedPath)
+        let suggestedPath = testBundle.path(forResource: "newerSuggested", ofType: "json")!
+        let suggestedUrl = URL(filePath: suggestedPath)
+
+        let now = Date("2025-04-28T20:50:00.000Z")!
+        try await importer.importOrefDetermination(enactedUrl: enactedUrl, suggestedUrl: suggestedUrl, now: now)
+
+        let determinations = try await coreDataStack.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "deliverAt",
+            ascending: false
+        ) as? [OrefDetermination] ?? []
+
+        #expect(determinations.count == 2) // two determinations, suggested is more recent than enacted
+
+        let suggested = determinations.first(where: { !$0.enacted && $0.deliverAt == $0.timestamp })!
+        let enacted = determinations.first(where: { $0.enacted })!
+
+        #expect(suggested.deliverAt == Date("2025-04-28T19:51:48.453Z"))
+        #expect(enacted.timestamp == Date("2025-04-28T19:41:48.453Z"))
+    }
+}
+
+extension Double {
+    func isApproximatelyEqual(to other: Double, epsilon: Double?) -> Bool {
+        // If no epsilon provided, require exact match
+        guard let epsilon = epsilon else {
+            return self == other
+        }
+
+        // Handle exact equality
+        if self == other {
+            return true
+        }
+
+        // Handle infinity and NaN
+        if isInfinite || other.isInfinite || isNaN || other.isNaN {
+            return self == other
+        }
+
+        // For values, use simple absolute difference
+        return abs(self - other) <= epsilon
+    }
+}

+ 41 - 0
scripts/pump-history-stats.py

@@ -0,0 +1,41 @@
+import json
+import sys
+
+def main():
+    pump_history = json.loads(sys.stdin.read())
+    bolus_total = 0.0
+    rate_total = 0.0
+    duration_total = 0.0
+    smb_count = 0
+    bolus_count = 0
+    temp_basal_count = 0
+    suspend_count = 0
+    resume_count = 0
+
+    for event in pump_history:
+        if 'amount' in event:
+            bolus_total += event['amount']
+            if event.get('isSMB', False):
+                smb_count += 1
+            bolus_count += 1
+        if 'rate' in event:
+            rate_total += event['rate']
+            temp_basal_count += 1
+        if 'duration (min)' in event:
+            duration_total += event['duration (min)']
+        if event['_type'] == 'PumpSuspend':
+            suspend_count += 1
+        if event['_type'] == 'PumpResume':
+            resume_count += 1
+
+    print(f'bolus_total: {bolus_total}')
+    print(f'rate_total: {rate_total}')
+    print(f'duration_total: {duration_total}')
+    print(f'smb_count: {smb_count}')
+    print(f'bolus_count: {bolus_count}')
+    print(f'temp_basal_count: {temp_basal_count}')
+    print(f'suspend_count: {suspend_count}')
+    print(f'resume_count: {resume_count}')
+
+if __name__ == '__main__':
+    main()