فهرست منبع

Alerts: PR review follow-ups (title suffixes, locale time, freshness window, audio loops until ack)

Deniz Cengiz 3 هفته پیش
والد
کامیت
b17125675b

+ 4 - 4
Trio/Sources/APS/APSManager.swift

@@ -1323,10 +1323,10 @@ final class BaseAPSManager: APSManager, Injectable {
                     String(localized: "Trio could not communicate with the pump. Check the pump and try again.")
                         + "\n\n\(inner.localizedDescription)"
                 )
-            case let .invalidPumpState(message): return (String(localized: "Pump State"), message)
-            case let .glucoseError(message): return (String(localized: "Glucose"), message)
-            case let .apsError(message): return (String(localized: "Algorithm"), message)
-            case let .manualBasalTemp(message): return (String(localized: "Manual Temp Basal"), message)
+            case let .invalidPumpState(message): return (String(localized: "Pump State Error"), message)
+            case let .glucoseError(message): return (String(localized: "Glucose Error"), message)
+            case let .apsError(message): return (String(localized: "Algorithm Error"), message)
+            case let .manualBasalTemp(message): return (String(localized: "Manual Temp Basal Active"), message)
             }
         }
         return ("Trio", error.localizedDescription)

+ 23 - 2
Trio/Sources/Modules/AlarmWindows/View/AlarmWindowsRootView.swift

@@ -64,14 +64,28 @@ extension AlarmWindows {
                             Button(
                                 action: {
                                     hintLabel = String(localized: "Day and Night Window")
+                                    let dayDefault = Self.formattedTimeOfDay(hour: 6, minute: 0)
+                                    let nightDefault = Self.formattedTimeOfDay(hour: 22, minute: 0)
                                     selectedVerboseHint = AnyView(
                                         VStack(alignment: .leading, spacing: 10) {
-                                            Text("Default: Day starts 06:00, Night starts 22:00.").bold()
+                                            Text(
+                                                String(
+                                                    format: String(localized: "Default: Day starts %1$@, Night starts %2$@."),
+                                                    dayDefault,
+                                                    nightDefault
+                                                )
+                                            ).bold()
                                             Text(
                                                 "These two times define the Day and Night windows. Each alarm's Active setting picks one — Day & Night, Day only, or Night only — and only fires when that window is current."
                                             )
                                             Text(
-                                                "The Night window runs from 'Night Starts' back around to 'Day Starts' — so by default, Night covers 22:00 through 06:00 the next morning."
+                                                String(
+                                                    format: String(
+                                                        localized: "The Night window runs from 'Night Starts' back around to 'Day Starts' — so by default, Night covers %1$@ through %2$@ the next morning."
+                                                    ),
+                                                    nightDefault,
+                                                    dayDefault
+                                                )
                                             )
                                             Text(
                                                 "These windows are shared between Glucose Alarms and Device Alarms."
@@ -110,5 +124,12 @@ extension AlarmWindows {
             let comps = Calendar.current.dateComponents([.hour, .minute], from: date)
             return TimeOfDay(hour: comps.hour ?? 0, minute: comps.minute ?? 0)
         }
+
+        /// Locale-aware HH:mm / h:mm a. Used so 12-hour locales see "10:00 PM"
+        /// instead of "22:00" in hint copy.
+        private static func formattedTimeOfDay(hour: Int, minute: Int) -> String {
+            let date = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) ?? Date()
+            return date.formatted(.dateTime.hour().minute())
+        }
     }
 }

+ 5 - 1
Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift

@@ -54,7 +54,11 @@ extension PumpConfig {
             trioAlertManager.acknowledgeAllOutstanding()
         }
 
-        func fireTestAlert(critical: Bool) {
+        // FIXME: Remove before merge. Dev-only buttons for exercising the
+    // foreground modal scheduler + background UN delivery paths. The body
+    // text is intentionally technobabble — it's for developers verifying
+    // pipeline wiring, not end users.
+    func fireTestAlert(critical: Bool) {
             let identifier = Alert.Identifier(
                 managerIdentifier: "Trio.test",
                 alertIdentifier: "test-\(UUID().uuidString.prefix(8))"

+ 6 - 9
Trio/Sources/Services/Alerts/CriticalAlertAudioPlayer.swift

@@ -9,25 +9,24 @@ final class CriticalAlertAudioPlayer {
     private let log = OSLog(subsystem: "org.nightscout.Trio", category: "CriticalAlertAudioPlayer")
 
     private var player: AVAudioPlayer?
-    private var stopTimer: Timer?
     private var vibrationTimer: Timer?
 
     private let vibrationInterval: TimeInterval = 2.0
     private let boostedVolume: Float = 1.0
     private let volumeBooster = SystemVolumeBooster()
-    private let maxDuration: TimeInterval = 6
 
     var isPlaying: Bool { player?.isPlaying ?? false }
 
     /// Start (or restart) looping playback of a bundled critical alert sound.
-    /// Safe to call repeatedly — replaces any current playback.
+    /// Loops until `stop()` is called from `retractAlert` /
+    /// `handleAcknowledgement` in `BaseTrioAlertManager` — modeled on the
+    /// LoopFollow / Jonas loop-failure alarm. `.playback` audio session gives
+    /// the app a privileged background state so the alarm continues even if
+    /// the user has the screen locked.
     func play(soundNamed soundName: String = "critical.caf") {
         stop()
 
         startVibration()
-        stopTimer = Timer.scheduledTimer(withTimeInterval: maxDuration, repeats: false) { [weak self] _ in
-            Task { @MainActor in self?.stop() }
-        }
 
         let resource = (soundName as NSString).deletingPathExtension
         let ext = (soundName as NSString).pathExtension.isEmpty ? "caf" : (soundName as NSString).pathExtension
@@ -82,11 +81,9 @@ final class CriticalAlertAudioPlayer {
 
     /// Stop playback if any. Safe to call when not playing.
     func stop() {
-        guard player != nil || stopTimer != nil || vibrationTimer != nil else { return }
+        guard player != nil || vibrationTimer != nil else { return }
         player?.stop()
         player = nil
-        stopTimer?.invalidate()
-        stopTimer = nil
         vibrationTimer?.invalidate()
         vibrationTimer = nil
         volumeBooster.restore()

+ 8 - 1
Trio/Sources/Services/Alerts/GlucoseAlertCoordinator.swift

@@ -37,6 +37,13 @@ final class GlucoseAlertCoordinator: Injectable {
     /// at the threshold boundary.
     private static let recoveryMarginMgDL: Decimal = 5
 
+    /// Readings older than this are considered stale and won't drive new
+    /// alarms — matches `APSManager`'s loop-input freshness gate (12 min,
+    /// allowing for one missed CGM transmission on a 5-min schedule).
+    /// Without this gate, force-quitting the app and reopening it after a
+    /// CGM blackout could fire an alarm based on a 19-min-old reading.
+    private static let readingFreshnessWindow: TimeInterval = 12 * 60
+
     /// Suppresses evaluations for a short window after launch. `firingAlertIDs`
     /// isn't persisted across relaunches, so without this quiet window the
     /// first reading after launch would re-fire any in-flight alarm whose
@@ -276,7 +283,7 @@ final class GlucoseAlertCoordinator: Injectable {
 
     private func fetchLatestReading() -> GlucoseStored? {
         do {
-            let cutoff = Date().addingTimeInterval(-20 * 60)
+            let cutoff = Date().addingTimeInterval(-Self.readingFreshnessWindow)
             let predicate = NSPredicate(format: "date >= %@", cutoff as NSDate)
             let results = try CoreDataStack.shared.fetchEntities(
                 ofType: GlucoseStored.self,

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

@@ -295,11 +295,14 @@ extension BaseUserNotificationsManager: alertMessageNotificationObserver {
         } else {
             content.title = message.title
         }
+        // Pump / algorithm / glucose subtypes used to route to dedicated UN
+        // identifiers (`pumpNotification`, `noLoopFirst/Second`, etc.) — all
+        // of those have moved into the unified `TrioAlertManager` pipeline.
+        // Only `.carb` keeps a dedicated identifier here so successive carb
+        // recommendations replace the previous one rather than stacking.
         switch message.subtype {
         case .carb:
             identifier = .carbsRequiredNotification
-        case .glucose:
-            identifier = .glucoseNotification
         default:
             identifier = .alertMessageNotification
         }