Timers Feature Guide
Guide to the Timers module — a server-authoritative, event-based timer system with timer lists, standalone timers, pomodoro tracking, and WebSocket-driven state sync.
Overview
The Timers module lets users group timers into timer lists (optionally looping and pomodoro-grouped) or run standalone timers that are not attached to a list. The server is the single source of truth: it stores planned duration plus the timestamps (startTime, pauseTime, accumulatedPausedMs) needed to derive elapsed/remaining time, rather than persisting per-second ticks. Clients control timers over HTTP and receive authoritative state changes over a WebSocket. A background worker completes timers even when no client is connected. The local UI runs a 1-second tick loop purely for smooth display and re-syncs whenever a server event arrives.
Architecture
Time is never stored as a running count — TimerTimeCalculator derives elapsed/remaining from server timestamps for both live requests and background completion checks.
Data Models
The shared client Timer model (duration in ms, remainingSeconds derived) is what flows over the API. The server entity Timers additionally persists the clock fields used for calculation: startTime, pauseTime, accumulatedPausedMs, updatedAt, plus optional listId, studySessionId, and workSessionId (the latter two back temporary study/work session timers). A TimerList may have a null listId on its timers, which makes those timers standalone.
TimerState Enum (state)
| Value | Meaning |
|---|---|
IDLE | Created but never started |
RUNNING | Counting down (server time advancing) |
PAUSED | Frozen; pauseTime set, accumulated on resume |
STOPPED | Halted/reset (server entity default) |
COMPLETED | Remaining reached 0 |
Connection State (TimerConnectionState)
| Value | Meaning |
|---|---|
Connected | WebSocket live; triggers re-sync of running timers |
Disconnected | No WebSocket connection |
Reconnecting | Attempting to re-establish |
Error(message) | Connection failed |
Local Playback Status (TimerPlaybackStatus)
Used only for the client tick loop in TimerPlaybackManager: Running, Paused, Completed, Stopped.
Time Calculation
TimerTimeCalculator derives time from timestamps (all stored as UTC):
- RUNNING:
elapsed = (nowServer − startTime) − accumulatedPausedMs - PAUSED:
elapsed = (pauseTime − startTime) − accumulatedPausedMs remaining = max(duration − elapsed, 0)startTimeis set on start and never reset on resume; resume adds the pause gap toaccumulatedPausedMs.
API Endpoints
All endpoints are mounted under /api/v1/timers and require authentication.
| Method | Path | Key Params | Description |
|---|---|---|---|
POST | /timers | body CreateTimerRequest (timerListId optional) | Create a timer in a list, or standalone if timerListId is null |
PATCH | /timers/{id} | body UpdateTimerRequest | Update a list timer |
DELETE | /timers/{id} | — | Delete a timer (204 No Content) |
GET | /timers/standalone | — | List the user’s standalone timers |
POST | /timers/standalone | body CreateTimerRequest | Create a standalone timer (201 Created) |
PATCH | /timers/standalone/{id} | body UpdateTimerRequest | Update a standalone timer |
POST | /timers/standalone/{id}/start | — | Start; 409 Conflict if another standalone timer is running |
POST | /timers/standalone/{id}/pause | clientTimestamp? | Pause a standalone timer |
POST | /timers/standalone/{id}/resume | clientTimestamp? | Resume a standalone timer |
POST | /timers/standalone/{id}/stop | clientTimestamp? | Stop a standalone timer |
POST | /timers/control/{listId}/start | timerId? | Start a timer in a list (first enabled if omitted) → List<Timer> |
POST | /timers/control/{listId}/pause | timerId?, clientTimestamp? | Pause running timer(s) → List<Timer> |
POST | /timers/control/{listId}/resume | clientTimestamp? | Resume the paused timer → List<Timer> |
POST | /timers/control/{listId}/stop | timerId?, clientTimestamp? | Stop active timer(s) → List<Timer> |
POST | /timers/control/{listId}/restart | timerId? | Restart from the beginning → List<Timer> |
GET | /timers/lists | — | List the user’s timer lists |
POST | /timers/lists | body CreateTimerListRequest | Create a timer list |
GET | /timers/lists/{id} | — | Get a timer list with its timers |
PATCH | /timers/lists/{id} | body CreateTimerListRequest | Update a timer list |
DELETE | /timers/lists/{id} | — | Delete a timer list (204 No Content) |
GET | /timers/settings | — | Get the user’s timer settings |
PUT | /timers/settings | body UpdateUserSettingsRequest | Update defaultTimerListId, dailyPomodoroGoal, notificationsEnabled |
WS | /timers/notifications | token? (query) or Authorization: Bearer | WebSocket for state changes + ping/pong |
duration is sent and stored in milliseconds. clientTimestamp is an optional epoch hint for pause/resume/stop accuracy. The WebSocket accepts the JWT either via the Authorization header or a token query parameter (for browser clients that cannot set WS headers).
State Machine
State transitions are authoritative on the server. startTime is preserved across pause/resume cycles; only accumulatedPausedMs grows.
Client Behavior
On Connected, the ViewModel calls syncRunningTimersFromServer() so reconnecting devices immediately reconcile to authoritative state. The tick loop never issues network calls — it only decrements local remainingMillis.
Key Files
| File | Purpose |
|---|---|
shared/.../models/Timer.kt | Client Timer model (duration ms, derived remainingSeconds) |
shared/.../models/TimerList.kt | TimerList model + indexInEnabledTimers() |
shared/.../models/timers/TimerState.kt | TimerState enum (IDLE/RUNNING/PAUSED/STOPPED/COMPLETED) |
shared/.../timer/TimerPlaybackManager.kt | Local 1s tick loop + TimerPlaybackState/TimerPlaybackStatus |
shared/.../timer/TimerConnectionState.kt | WebSocket connection state |
shared/.../timer/TimerWebSocketServerMessage.kt | Server→client WS messages (TimerUpdate, Pong, AgendaRefresh, …) |
shared/.../services/timers/TimerService.kt | Ktor HTTP client for timer endpoints |
shared/.../websocket/TimerWebSocketClient.kt | WebSocket connection + message handling |
timers/timers_domain/.../use_case/TimerUseCases.kt | Aggregated use cases (control + CRUD + standalone) |
timers/timers_domain/.../repository/TimersRepository.kt | Repository interface |
timers/timers_data/.../repository/TimersRepositoryImpl.kt | Repository implementation (token + service calls) |
timers/timers_presentation/.../viewmodel/TimerViewModel.kt | MVI ViewModel coordinating HTTP + WS + playback |
timers/timers_presentation/.../viewmodel/state/TimerState.kt | TimerState UI state |
timers/timers_presentation/.../ui/intent/TimerIntent.kt | TimerIntent / TimerEffect |
timers/timers_presentation/.../service/TimerForegroundService.kt | Android foreground service for running timers |
server/.../routing/TimerRouting.kt | All REST + WebSocket endpoints |
server/.../service/TimerService.kt | Server-side timer business logic |
server/.../service/TimerTimeCalculator.kt | Timestamp-based elapsed/remaining/shouldComplete |
server/.../service/TimerNotifier.kt | WebSocket session registry + broadcasting |
server/.../service/TimerCheckerService.kt | Background completion + reminder worker |
server/.../database/entities/Timers.kt | Timers / TimerLists Exposed tables + entities |
Testing
- Unit (server):
TimerTimeCalculatorTest— running elapsed growth, paused freeze, multiple pause/resume cycles accumulating correctly,shouldCompleteat/after zero, coercion to non-negative values. - Timezone:
TimerCheckerTimezoneTest— timestamps treated as UTC; no drift across zones. - Background completion:
TimerCheckerServiceEventDrivenTest— running timers complete and broadcast even with no client connected. - List ordering:
TimerListIndexInEnabledTimersTest—indexInEnabledTimers()skips disabled timers and returns -1 when absent/disabled. - WebSocket: ping/pong time sync, one client’s control action propagating to another client, reconnect re-sync via
syncRunningTimersFromServer().
Troubleshooting
Timer not completing
- Confirm
TimerCheckerServiceis running on the server. - Verify
startTimeis set andstate == RUNNING. - Check
TimerTimeCalculator.shouldComplete()— completion is server-authoritative and does not require a connected client.
Pause/resume produces wrong remaining time
- Verify
accumulatedPausedMsgrows on each resume (gap =now − pauseTime). - Ensure
startTimeis not being reset on resume — it must persist for the lifetime of the run.
Time drifts between devices
- Ensure ping/pong is flowing; the client derives
serverTimeOffsetfromPong.serverTime. - The offset is display-only — authoritative time always comes from the server.
Standalone timer returns 409 Conflict on start
- Only one standalone timer may run at a time; stop or pause the currently running one first (
StandaloneTimerConflictException).
Timer state out of sync in the UI
- Check
connectionState; on reconnect the ViewModel re-syncs running timers. - Confirm
TimerUpdateWS events are received andTimerPlaybackManager.overridePlaybackState()is applied.
WebSocket connection rejected (401)
- The connection needs a valid JWT either in the
Authorization: Bearerheader or as atokenquery parameter; browser clients must use the query parameter.
API Endpoints
All endpoints require JWT authentication and are prefixed with /api/v1. See server/README.md for full reference.
| Method | Path | Description |
|---|---|---|
GET | /timers/lists | List timer lists |
POST | /timers/lists | Create a timer list |
GET | /timers/lists/{id} | Get timer list by ID |
PATCH | /timers/lists/{id} | Update timer list |
DELETE | /timers/lists/{id} | Delete timer list |
POST | /timers/control/{listId}/start | Start a timer in a list |
POST | /timers/control/{listId}/pause | Pause a running timer |
POST | /timers/control/{listId}/resume | Resume a paused timer |
POST | /timers/control/{listId}/stop | Stop a running timer |
POST | /timers/control/{listId}/restart | Restart a timer |
GET | /timers/standalone | List standalone timers |
POST | /timers/standalone | Create standalone timer |
WebSocket | /timers/notifications | Real-time timer notifications |
Related Docs
- Timer system — server-authoritative timer system deep dive (WebSocket message schemas, migration notes)
- Domain & models — shared domain model reference
- UI & UX — timer screen layouts and controls
- Testing & QA — testing strategy