diff --git a/backend/app/routers/sessions.py b/backend/app/routers/sessions.py
index 4e9dd0c..454389b 100644
--- a/backend/app/routers/sessions.py
+++ b/backend/app/routers/sessions.py
@@ -191,18 +191,30 @@ async def timer_action(
BREAK_EVENTS = {"break_start", "break_pause", "break_resume", "break_reset"}
if body.event_type in BREAK_EVENTS:
block_id = body.block_id or session.current_block_id
- event = TimerEvent(
+
+ # When break starts, implicitly pause the main block timer so elapsed
+ # time is captured accurately in the activity log and on page reload.
+ if body.event_type == "break_start" and block_id:
+ db.add(TimerEvent(
+ session_id=session.id,
+ block_id=block_id,
+ event_type="pause",
+ ))
+
+ db.add(TimerEvent(
session_id=session.id,
block_id=block_id,
event_type=body.event_type,
- )
- db.add(event)
+ ))
await db.commit()
await db.refresh(session)
break_elapsed_seconds = 0
+ block_elapsed_seconds = 0
if body.event_type in ("break_start", "break_reset") and block_id:
break_elapsed_seconds, _ = await compute_break_elapsed(db, session.id, block_id)
+ if body.event_type == "break_start" and block_id:
+ block_elapsed_seconds, _ = await compute_block_elapsed(db, session.id, block_id)
ws_payload = {
"event": body.event_type,
@@ -211,6 +223,7 @@ async def timer_action(
"current_block_id": session.current_block_id,
"is_active": session.is_active,
"break_elapsed_seconds": break_elapsed_seconds,
+ "block_elapsed_seconds": block_elapsed_seconds,
}
await manager.broadcast(session.child_id, ws_payload)
return session
diff --git a/frontend/src/components/TimerDisplay.vue b/frontend/src/components/TimerDisplay.vue
index f2498a3..5c19e9e 100644
--- a/frontend/src/components/TimerDisplay.vue
+++ b/frontend/src/components/TimerDisplay.vue
@@ -58,10 +58,12 @@ const elapsed = computed(() => {
const remaining = computed(() => Math.max(0, blockDuration.value - elapsed.value))
const display = computed(() => {
- const s = remaining.value
- const m = Math.floor(s / 60)
- const sec = s % 60
- return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
+ const s = blockDuration.value - elapsed.value
+ if (s < 0) {
+ const abs = Math.abs(s)
+ return `-${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`
+ }
+ return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
})
const label = computed(() => {
@@ -72,7 +74,7 @@ const label = computed(() => {
const CIRCUMFERENCE = 2 * Math.PI * 88
const dashOffset = computed(() => {
if (!blockDuration.value) return CIRCUMFERENCE
- const pct = remaining.value / blockDuration.value
+ const pct = Math.max(0, remaining.value / blockDuration.value)
return CIRCUMFERENCE * (1 - pct)
})
diff --git a/frontend/src/stores/schedule.js b/frontend/src/stores/schedule.js
index 0c87024..6843695 100644
--- a/frontend/src/stores/schedule.js
+++ b/frontend/src/stores/schedule.js
@@ -95,6 +95,12 @@ export const useScheduleStore = defineStore('schedule', () => {
if (event.block_id) breakElapsedCache.value[event.block_id] = elapsed
breakStartedAt.value = Date.now()
isBreakMode.value = true
+ // Sync main block timer to server-authoritative elapsed and mark paused
+ const blockElapsed = event.block_elapsed_seconds ?? blockElapsedCache.value[event.block_id] ?? blockElapsedOffset.value
+ blockElapsedOffset.value = blockElapsed
+ if (event.block_id) blockElapsedCache.value[event.block_id] = blockElapsed
+ blockStartedAt.value = null
+ isPaused.value = true
}
if (event.event === 'break_pause') {
if (breakStartedAt.value) {
@@ -140,12 +146,9 @@ export const useScheduleStore = defineStore('schedule', () => {
}
blockStartedAt.value = Date.now()
isPaused.value = false
- // Switching to a new block clears break mode
- if (event.block_id !== event.current_block_id || !isBreakMode.value) {
- isBreakMode.value = false
- breakStartedAt.value = null
- breakElapsedOffset.value = breakElapsedCache.value[event.block_id] ?? 0
- }
+ isBreakMode.value = false
+ breakStartedAt.value = null
+ breakElapsedOffset.value = breakElapsedCache.value[event.block_id] ?? 0
}
// Reset — clear elapsed to 0 and start counting immediately
if (event.event === 'reset') {
@@ -175,6 +178,8 @@ export const useScheduleStore = defineStore('schedule', () => {
if (event.event === 'resume') {
blockStartedAt.value = Date.now()
isPaused.value = false
+ isBreakMode.value = false
+ breakStartedAt.value = null
}
// Timer events update session state
if (event.current_block_id !== undefined && session.value) {
@@ -256,15 +261,51 @@ export const useScheduleStore = defineStore('schedule', () => {
// Start the timer for the currently selected block (optimistic).
function startCurrentBlock(sessionId) {
if (!session.value?.current_block_id) return
+ const blockId = session.value.current_block_id
+ if (isBreakMode.value) {
+ if (breakStartedAt.value) {
+ breakElapsedOffset.value += Math.floor((Date.now() - breakStartedAt.value) / 1000)
+ }
+ breakElapsedCache.value[blockId] = breakElapsedOffset.value
+ breakStartedAt.value = null
+ isBreakMode.value = false
+ sendTimerAction(sessionId, 'break_pause', blockId)
+ }
isPaused.value = false
blockStartedAt.value = Date.now()
- sendTimerAction(sessionId, 'start', session.value.current_block_id)
+ sendTimerAction(sessionId, 'start', blockId)
+ }
+
+ // Resume the timer for the currently selected block (optimistic).
+ function resumeCurrentBlock(sessionId) {
+ if (!session.value?.current_block_id) return
+ const blockId = session.value.current_block_id
+ if (isBreakMode.value) {
+ if (breakStartedAt.value) {
+ breakElapsedOffset.value += Math.floor((Date.now() - breakStartedAt.value) / 1000)
+ }
+ breakElapsedCache.value[blockId] = breakElapsedOffset.value
+ breakStartedAt.value = null
+ isBreakMode.value = false
+ sendTimerAction(sessionId, 'break_pause', blockId)
+ }
+ blockStartedAt.value = Date.now()
+ isPaused.value = false
+ sendTimerAction(sessionId, 'resume')
}
// Break timer actions
function startBreak(sessionId) {
if (!session.value?.current_block_id) return
const blockId = session.value.current_block_id
+ // Pause the main block timer
+ if (blockStartedAt.value) {
+ blockElapsedOffset.value += Math.floor((Date.now() - blockStartedAt.value) / 1000)
+ }
+ blockElapsedCache.value[blockId] = blockElapsedOffset.value
+ blockStartedAt.value = null
+ isPaused.value = true
+ // Start break timer
isBreakMode.value = true
breakElapsedOffset.value = breakElapsedCache.value[blockId] ?? 0
breakStartedAt.value = Date.now()
@@ -332,6 +373,7 @@ export const useScheduleStore = defineStore('schedule', () => {
switchBlock,
selectBlock,
startCurrentBlock,
+ resumeCurrentBlock,
resetCurrentBlock,
startBreak,
pauseBreak,
diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue
index 1ea8e37..fec0a03 100644
--- a/frontend/src/views/DashboardView.vue
+++ b/frontend/src/views/DashboardView.vue
@@ -88,7 +88,7 @@