Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/LageBuch.AppLogic/ViewModels/ReminderViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand All @@ -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(
Expand Down
32 changes: 30 additions & 2 deletions tests/LageBuch.AppLogic.Tests/ReminderViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);

Expand All @@ -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()
{
Expand Down
Loading