diff --git a/backend/app/routers/sessions.py b/backend/app/routers/sessions.py index a093829..d4bc51b 100644 --- a/backend/app/routers/sessions.py +++ b/backend/app/routers/sessions.py @@ -189,6 +189,29 @@ async def timer_action( await db.commit() await db.refresh(session) + # For 'start' events, compute elapsed from previous intervals so every + # client (including TV) can restore the correct offset without a cache. + block_elapsed_seconds = 0 + if body.event_type == "start" and event.block_id: + tick_result = await db.execute( + select(TimerEvent) + .where( + TimerEvent.session_id == session.id, + TimerEvent.block_id == event.block_id, + TimerEvent.event_type.in_(["start", "resume", "pause"]), + TimerEvent.id != event.id, + ) + .order_by(TimerEvent.occurred_at) + ) + tick_events = tick_result.scalars().all() + last_start_time = None + for e in tick_events: + if e.event_type in ("start", "resume"): + last_start_time = e.occurred_at + elif e.event_type == "pause" and last_start_time: + block_elapsed_seconds += int((e.occurred_at - last_start_time).total_seconds()) + last_start_time = None + # Broadcast the timer event to all TV clients ws_payload = { "event": body.event_type, @@ -196,6 +219,7 @@ async def timer_action( "block_id": event.block_id, "current_block_id": session.current_block_id, "is_active": session.is_active, + "block_elapsed_seconds": block_elapsed_seconds, } await manager.broadcast(session.child_id, ws_payload) diff --git a/frontend/src/stores/schedule.js b/frontend/src/stores/schedule.js index e5dc49b..b54ae3f 100644 --- a/frontend/src/stores/schedule.js +++ b/frontend/src/stores/schedule.js @@ -86,9 +86,12 @@ export const useScheduleStore = defineStore('schedule', () => { blockStartedAt.value = null isPaused.value = true } - // Start — restore cached elapsed if returning to a previously worked block + // Start — use server-provided elapsed (authoritative for all clients incl. TV), + // fall back to local cache, then 0 for a fresh block if (event.event === 'start') { - blockElapsedOffset.value = blockElapsedCache.value[event.block_id] || 0 + const elapsed = event.block_elapsed_seconds ?? blockElapsedCache.value[event.block_id] ?? 0 + blockElapsedOffset.value = elapsed + if (event.block_id) blockElapsedCache.value[event.block_id] = elapsed blockStartedAt.value = Date.now() isPaused.value = false } diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 80cec59..ce474b6 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -236,6 +236,10 @@ async function selectBlock(block) { await scheduleStore.sendTimerAction(scheduleStore.session.id, 'pause', currentId) } + // Optimistically update so the UI switches immediately without waiting for the WS start event + scheduleStore.session.current_block_id = block.id + scheduleStore.isPaused = false + scheduleStore.sendTimerAction(scheduleStore.session.id, 'start', block.id) }