Skip to content

Add configurable timer ring duration and repeat reminders - #865

Open
Marcus2626 wants to merge 8 commits into
Ebullioscopic:devfrom
Marcus2626:feature/timer-repeat-alerts
Open

Marcus2626 wants to merge 8 commits into
Ebullioscopic:devfrom
Marcus2626:feature/timer-repeat-alerts

Conversation

@Marcus2626

@Marcus2626 Marcus2626 commented Oct 1, 2026 •

Copy link
Copy Markdown

Closes #859

Summary

Atoll's built-in timer currently rings continuously after expiry until manually stopped. This change adds a configurable ring duration and silent interval, allowing reminders to repeat while the timer remains in overtime.

For example, with a 30-second ring duration and a 5-minute interval, the timer rings for 30 seconds, stays silent for 5 minutes, then rings again until dismissed.

Changes

  • Add ring-duration controls in minutes and seconds and a repeat interval in minutes to the existing Timer settings. Defaults are 30 seconds and 5 minutes, with English and Simplified Chinese labels.
  • Reuse TimerManager's one-second countdown tick to advance reminders. TimerAlertController only stores ringing/silent phase state; audio playback remains in TimerManager. The timer uses the common RunLoop mode so updates continue during UI tracking.
  • Measure the silent interval from when playback stops. Saved setting changes also affect the current phase.
  • Cancel reminders when the timer is stopped, replaced, reset, or paused. Resuming an overtime timer starts a fresh ring.
  • Silence playback before sleep and advance at most one phase on wake, without replaying missed reminders.
  • Reject queued callbacks from an old countdown by checking the current Timer instance. Keep cleanup in the existing session and pause methods.
  • Add regression coverage and run the alert checks in CI.

The existing public timer method signatures and macOS Clock integration remain unchanged. These settings apply to Atoll's built-in timer, not the Clock app's alarms. Alert transitions are checked once per second and may be delayed if the main thread is busy.

Validation

  • Full Debug arm64 application build passed with code signing disabled.
  • bash tests/run_timer_alert_regression.sh passed: phase boundaries, live setting changes, replacement, cancellation, simulated sleep/wake, delayed updates, and invalid persisted-value bounds.
  • python3 -m unittest discover -s tests -v passed: 7 tests, including the existing timer lifecycle regression.
  • A temporary integration harness using the actual countdown/update methods and a real RunLoop passed stale-callback rejection, expiry, automatic silencing, and stop checks with simulated audio.
  • git diff --check passed for the committed changes.

Settings preview

Previously captured settings preview; the same controls are retained in this implementation:

Timer alert settings

Summary by CodeRabbit

  • New Features
    • Added repeating timer alerts that ring for 30 seconds and remain silent for 5 minutes by default. Alerts continue during overtime until stopped, and pausing the timer silences them.
    • Added settings to customize ring duration (1 second to 60 minutes) and the repeat interval (1 minute to 24 hours).
    • Alert settings can be changed while a reminder is active, and the new settings are available in English and Simplified Chinese.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: a168eb07-a54a-46a0-a246-6e5d5b1e2b3b

📥 Commits

Reviewing files that changed from the base of the PR and between a4e9b53 and a573f3f.

📒 Files selected for processing (4)
  • DynamicIsland/components/Settings/SettingsView.swift
  • DynamicIsland/managers/TimerAlertController.swift
  • DynamicIsland/managers/TimerManager.swift
  • tests/TimerAlertRegression.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • DynamicIsland/managers/TimerAlertController.swift
  • tests/TimerAlertRegression.swift
  • DynamicIsland/components/Settings/SettingsView.swift
  • DynamicIsland/managers/TimerManager.swift

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The built-in timer now supports configurable repeating alerts with persisted ring-duration and repeat-interval settings. TimerManager coordinates alert phases and sound playback across timer ticks, lifecycle changes, and sleep or wake. A regression executable validates alert timing and behavior, and CI runs it.

Changes

Built-in timer alerts

Layer / File(s) Summary
Alert settings and preferences
DynamicIsland/models/Constants.swift, DynamicIsland/components/Settings/SettingsView.swift, DynamicIsland/Localizable.xcstrings, CHANGELOG.md
Adds persisted duration and interval preferences, timer settings controls, and English and Simplified Chinese labels and explanatory text. The changelog describes the repeat behavior and defaults.
Alert phase timing
DynamicIsland/managers/TimerAlertController.swift
Adds controller operations to start, stop, prepare for sleep, and update ringing and silent phases. The controller clamps duration to 1–3,600 seconds and interval to 1–1,440 minutes.
Timer lifecycle and sound integration
DynamicIsland/managers/TimerManager.swift
Integrates alert updates with manual countdown ticks, timer lifecycle actions, sleep and wake notifications, and sound playback. Countdown ticks reject stale timers and inactive or paused timers.
Regression checks and CI runner
tests/TimerAlertRegression.swift, tests/run_timer_alert_regression.sh, .github/workflows/ci.yml, .gitignore
Adds simulated-time checks for alert timing, setting changes, replacement alerts, sleep and wake, delayed updates, and integer-boundary settings. Adds a runner and invokes it in CI.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant TimerManager
  participant TimerAlertController
  participant SoundPlayer
  TimerManager->>TimerAlertController: Start alert when timer reaches zero
  loop On timer ticks
    TimerManager->>TimerAlertController: Update alert phase with configured duration and interval
    TimerAlertController-->>TimerManager: Return phase change or no change
    TimerManager->>SoundPlayer: Play or stop sound for the phase
  end
Loading

Merge Risk: ⚪ Minimal · up to a573f

The built-in timer gains configurable repeating alerts. No concrete merge-blocking risk was identified in the supplied context, so the change looks ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 7acf1

The change is confined to the built-in timer and its existing audio playback. Bounded timing settings, session cleanup, and rejection of obsolete callbacks limit its effects. No new security issue was identified in the reviewed paths, although complete caller and runtime coverage was not established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is the application's single built-in timer session and local audio output. Preference manipulation can alter bounded reminder timing, and repeats invoke the existing sound-file decoder again. The reviewed flow does not establish additional tenant, service, credential, or data-store authority.

Trust Boundaries and Controls

  • observed — Persisted timing values are clamped before use. Countdown callbacks require matching Timer identity, manual source, active state, and an unpaused session before changing alert state. These checks contain stale work within its originating timer session.

Resilience and Maintainability Implications

  • observed — Sleep handling stops audio and converts a ringing phase to silence. Wake requests one update, while the controller resets its phase anchor on transition, preventing a burst of missed reminders. Stop and replacement clear phase state rather than retaining deferred reminders.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies the coding requirements in #859. It adds persisted defaults of 30 seconds and 5 minutes, bounded settings controls, and English and Chinese UI text. TimerAlertController implements …
Out of Scope Changes check ✅ Passed The changes stay within #859. The settings UI, localization, timer alert controller, timer integration, persistence, regression tests, and CI workflow directly support configurable repeat alerts for t…
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 6 files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: configurable timer ring duration and repeat reminders.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@Marcus2626

Copy link
Copy Markdown
Author

Function-level implementation notes

This PR changes Atoll's built-in timer. On expiry, it rings for the configured duration, remains silent for the configured interval, and repeats until dismissed. Overtime remains visible during silent periods. The defaults are 30 seconds of ringing and 5 minutes of silence.

TimerAlertController.swift

TimerAlertController stores the start time of the current alert phase and whether it is ringing. TimerManager continues to own the countdown timer and audio player.

Function Behavior
start(at:) Begins a ringing phase at the supplied time. Accepting a time also allows tests to use a simulated clock.
stop() Clears the alert phase, preventing later reminders. TimerManager stops the audio player separately.
prepareForSleep(at:) If ringing, moves to a silent phase starting at sleep time. An existing silent phase keeps its original start time.
update(at:duration:interval:) Checks whether the current phase has elapsed. Ring duration is bounded to 1–3600 seconds and the silent interval to 1–1440 minutes. Returns true to start playback, false to stop it, or nil when nothing changes. Each new phase starts at the current update time, so delayed ticks do not replay missed reminders in a burst. Settings are read again on each update, so edits affect an active phase.

TimerManager.swift

Function Change
init() Observes macOS sleep and wake. Before sleep, it silences an active ring; on wake, it checks the alert phase once.
startTimer(duration:name:preset:) Invalidates the previous countdown, starts a new lifecycle session, and calls the shared scheduleCountdown(). The new session clears any previous alert and sound.
stopTimer() and forceStopTimer() Continue through the existing reset path. endTimerSession() now clears the alert and stops playback, cancelling future reminders.
pauseTimer() Clears the alert, stops playback, and invalidates the countdown timer.
resumeTimer() Restarts the shared countdown. When resuming in overtime, it begins a fresh ring.
scheduleCountdown() Replaces the duplicated start and resume tick closures with one one-second timer. At expiry, it enters overtime once and starts ringing; later ticks update overtime and the alert phase. Each queued MainActor callback verifies that its Timer instance is still current, preventing an old countdown from changing a stopped, paused, or replaced timer. The timer runs in the common RunLoop mode so UI tracking does not suspend updates.
adoptExternalTimer(...) Uses the existing new-session path, which clears an Atoll alert before adopting a Clock timer.
beginTimerSession() and endTimerSession() Centralize alert cancellation and audio stop alongside the existing lifecycle cleanup.
updateTimerAlert() Reads the saved settings on each tick, asks the controller whether the phase changed, then starts or stops playback.
playTimerSound() Stops and releases any previous player before starting a new ring. It retains the existing custom-sound and fallback selection; playback loops until the ringing phase ends.

Settings and validation

TimerSettings adds the Timer Alerts section using the project's existing controls. Its minute and second bindings edit one duration stored in seconds while preserving the other component. The input ranges exclude a zero-second ring and values above one hour. Constants.swift supplies the defaults, and Localizable.xcstrings supplies English and Simplified Chinese labels.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add configurable repeat alerts to the built-in timer

1 participant