5.8 KiB
5.8 KiB
Feature: Timer Breaks (Pause During Work)
Overview
Allow users to take breaks while a timer is running. When on break, elapsed time is frozen and break time is tracked. When resumed, break time accumulates and is subtracted from the displayed work time.
User Experience
Timer States
- Running — normal state, elapsed time ticking
- On Break — elapsed time frozen, break time ticking, Stop/Cancel buttons disabled
- Stopped — no timer active
UI Changes (TimerWidget)
- Add a "Break" button (amber,
Pauseicon) next to Stop when timer is running - When on break:
- Change pulsing dot color from red to amber
- Elapsed time frozen at net work time
- Show break time below:
Break: Xm XXs(live-ticking) - Replace "Break" button with "Resume" button (green,
Playicon) - Disable Stop and Cancel buttons (tooltip: "Resume before stopping")
Duration Calculations
- Work time (displayed):
now - startTime - totalBreakSeconds- Where
totalBreakSeconds = (breakMinutes * 60) + (now - breakStart if on break) - When on break: frozen at
(breakStart - startTime - breakMinutes * 60)
- Where
- Break time (displayed):
breakMinutes * 60 + (now - breakStart if on break)
Implementation
1. Database Schema (backend/prisma/schema.prisma)
Add two fields to OngoingTimer:
model OngoingTimer {
// ... existing fields ...
breakMinutes Int @default(0) @map("break_minutes")
breakStart DateTime? @map("break_start") @db.Timestamptz()
}
Run: npx prisma migrate dev --name add_timer_break_fields
2. Backend Service (backend/src/services/timer.service.ts)
New method startBreak(userId):
- Get ongoing timer, throw
NotFoundErrorif none - Check
timer.breakStartis null (not already on break), throwBadRequestErrorif on break - Update:
breakStart = new Date() - Return updated timer
New method endBreak(userId):
- Get ongoing timer, throw
NotFoundErrorif none - Check
timer.breakStartis not null, throwBadRequestErrorif not on break - Calculate additional break minutes:
Math.floor((now - breakStart) / 60000) - Update:
breakMinutes += additionalMinutes,breakStart = null - Return updated timer
Modify stop(userId):
- Before creating time entry, check
timer.breakStartis null — throwBadRequestError("Cannot stop timer while on break")if break is active - When creating
TimeEntry, setbreakMinutes: timer.breakMinutes
Modify cancel(userId):
- Check
timer.breakStartis null — throwBadRequestError("Cannot cancel timer while on break")if break is active
3. Backend Routes (backend/src/routes/timer.routes.ts)
Add two new routes (both require auth, no body validation):
POST /api/timer/break → timerService.startBreak(userId)
POST /api/timer/resume → timerService.endBreak(userId)
4. MCP Tools (backend/src/routes/mcp.routes.ts)
Add two MCP tools: pause_timer and resume_timer.
5. Frontend Types (frontend/src/types/index.ts)
Update OngoingTimer interface:
export interface OngoingTimer {
// ... existing fields ...
breakMinutes: number;
breakStart: string | null;
}
6. Frontend API (frontend/src/api/timer.ts)
Add two methods:
startBreak: async (): Promise<OngoingTimer> => { ... }
endBreak: async (): Promise<OngoingTimer> => { ... }
7. Frontend TimerContext (frontend/src/contexts/TimerContext.tsx)
- Add
breakSecondsstate (live-updating, similar toelapsedSeconds) - Expose
isOnBreakderived boolean (ongoingTimer?.breakStart !== null) - Update elapsed time calculation:
- Running:
(now - startTime) - (breakMinutes * 60) - (now - breakStart if on break) - On break:
(breakStart - startTime) - (breakMinutes * 60)(frozen)
- Running:
- Break seconds:
(breakMinutes * 60) + (now - breakStart if on break) - Add
startBreak()andendBreak()callbacks - Expose
breakSecondsandisOnBreakin context value
8. Frontend TimerWidget (frontend/src/components/TimerWidget.tsx)
- Import
Pauseicon from lucide-react - Add Break/Resume button between project selector and Stop button
- Show break time display when
breakSeconds > 0orisOnBreak - Change dot color to amber when on break
- Disable Stop/Cancel when on break with tooltip
Files to Modify (in order)
| # | File | Change |
|---|---|---|
| 1 | backend/prisma/schema.prisma |
Add breakMinutes, breakStart to OngoingTimer |
| 2 | backend/src/services/timer.service.ts |
Add startBreak(), endBreak(), modify stop() and cancel() |
| 3 | backend/src/routes/timer.routes.ts |
Add /break and /resume routes |
| 4 | backend/src/routes/mcp.routes.ts |
Add pause_timer and resume_timer MCP tools |
| 5 | frontend/src/types/index.ts |
Add breakMinutes, breakStart to OngoingTimer |
| 6 | frontend/src/api/timer.ts |
Add startBreak(), endBreak() API methods |
| 7 | frontend/src/contexts/TimerContext.tsx |
Add break state, breakSeconds, isOnBreak, break methods |
| 8 | frontend/src/components/TimerWidget.tsx |
Add break UI (button, display, disabled states) |
Edge Cases
- Break start must be after timer start (always true since break is clicked after start)
- Break duration naturally cannot exceed work duration (breakStart > startTime)
- On stop: reject if break is active (user must resume first)
- On cancel: reject if break is active (user must resume first)
- Break minutes accumulate across multiple break/resume cycles
- Timer refetch (every 60s) will sync break state from server
Verification
- Run
npm run lintin bothfrontend/andbackend/ - Run
npm run buildin bothfrontend/andbackend/ - Manual testing: start timer → break → verify elapsed frozen, break ticking → resume → verify break added to total → stop → verify time entry has correct breakMinutes