diff --git a/src/LageBuch.AppLogic/ViewModels/ReminderViewModel.cs b/src/LageBuch.AppLogic/ViewModels/ReminderViewModel.cs index 61f82fa..1017e9d 100644 --- a/src/LageBuch.AppLogic/ViewModels/ReminderViewModel.cs +++ b/src/LageBuch.AppLogic/ViewModels/ReminderViewModel.cs @@ -28,9 +28,11 @@ public sealed partial class ReminderViewModel : ObservableObject, IDisposable // close+reopen or a crash instead of restarting a fresh cycle. private const string TimerKey = "ils-reminder"; - // The spoken cue fires once when a cycle falls due; this guards against re-announcing it on - // every subsequent tick until the next Acknowledge opens a fresh cycle. - private bool _dueAnnounced; + // The spoken cue repeats while a cycle sits unacknowledged, so it stays insistent instead of + // being said once and forgotten; this tracks when it last played so OnTick only re-plays once + // RepeatInterval has passed, and Acknowledge clears it to announce immediately on the next cycle. + private static readonly TimeSpan RepeatInterval = TimeSpan.FromSeconds(60); + private DateTimeOffset? _lastAnnouncedAt; public ReminderViewModel( IIncidentSession session, IClock clock, ITicker ticker, IAlarmService alarm, Action onChanged, @@ -77,11 +79,13 @@ public string RemainingDisplay private void OnTick() { - // Announce the moment the cycle crosses due, exactly once per cycle. - if (_timer.IsDue(_clock.Now) && !_dueAnnounced) + // Announce the moment the cycle crosses due, then keep repeating on RepeatInterval until + // acknowledged, so the reminder stays insistent instead of being said once and forgotten. + if (_timer.IsDue(_clock.Now) && + (_lastAnnouncedAt is null || _clock.Now - _lastAnnouncedAt >= RepeatInterval)) { _alarm.Play(AlarmSound.IlsReminderDue); - _dueAnnounced = true; + _lastAnnouncedAt = _clock.Now; } OnPropertyChanged(nameof(RemainingDisplay)); @@ -95,7 +99,7 @@ private void OnTick() private void Acknowledge() { _timer.Acknowledge(_clock); - _dueAnnounced = false; + _lastAnnouncedAt = null; PersistTimer(); // durable anchor for the new (recurring) cycle // "Von" is us — the logged-in operator's call sign (e.g. the ELW's Funkrufname). _session.AddJournalEntry( diff --git a/tests/LageBuch.AppLogic.Tests/ReminderViewModelTests.cs b/tests/LageBuch.AppLogic.Tests/ReminderViewModelTests.cs index 8e25c64..a5a7107 100644 --- a/tests/LageBuch.AppLogic.Tests/ReminderViewModelTests.cs +++ b/tests/LageBuch.AppLogic.Tests/ReminderViewModelTests.cs @@ -114,7 +114,7 @@ public void Dispose_unsubscribes_from_ticker() } [Fact] - public void Becoming_due_plays_the_spoken_cue_once_per_cycle() + public void Becoming_due_plays_the_spoken_cue_immediately() { var (session, clock) = NewSession(); var ticker = new FakeTicker(); @@ -123,7 +123,6 @@ public void Becoming_due_plays_the_spoken_cue_once_per_cycle() clock.Now = T0.AddMinutes(15); ticker.Fire(); - ticker.Fire(); // still due — must not re-announce within the same cycle Assert.Equal(new[] { AlarmSound.IlsReminderDue }, alarm.Played); @@ -135,6 +134,35 @@ public void Becoming_due_plays_the_spoken_cue_once_per_cycle() Assert.Equal(new[] { AlarmSound.IlsReminderDue, AlarmSound.IlsReminderDue }, alarm.Played); } + [Fact] + public void Unacknowledged_reminder_repeats_the_spoken_cue_every_60_seconds() + { + var (session, clock) = NewSession(); + var ticker = new FakeTicker(); + var alarm = new FakeAlarmService(); + var vm = new ReminderViewModel(session, clock, ticker, alarm, () => { }, firstIntervalMinutes: 15, recurringIntervalMinutes: 30); + + clock.Now = T0.AddMinutes(15); + ticker.Fire(); + Assert.Single(alarm.Played); + + // Still due, but less than 60s since the last announcement — no repeat yet. + clock.Now = T0.AddMinutes(15).AddSeconds(30); + ticker.Fire(); + Assert.Single(alarm.Played); + + // 60s since the last announcement — repeats. + clock.Now = T0.AddMinutes(15).AddSeconds(60); + ticker.Fire(); + Assert.Equal(2, alarm.Played.Count); + + // Acknowledging stops the repeat until the next cycle falls due. + vm.AcknowledgeCommand.Execute(null); + clock.Now = T0.AddMinutes(15).AddSeconds(90); + ticker.Fire(); + Assert.Equal(2, alarm.Played.Count); + } + [Fact] public void Follow_up_cycle_uses_the_recurring_interval_after_acknowledge() {