Fix double-click to switch blocks and TV elapsed reset

Double-click fix:
- After awaiting the pause, optimistically set current_block_id and
  isPaused on the store so the UI switches instantly. The subsequent
  WS start event confirms the state without requiring a second click.

TV elapsed reset fix:
- The TV's local cache was empty for blocks paused before it connected,
  so returning to those blocks showed 0 elapsed.
- Backend now computes the block's accumulated elapsed from previous
  start/pause cycles and includes it as block_elapsed_seconds in the
  'start' WS event payload.
- All clients (Dashboard and TV) now use this authoritative server value
  instead of relying solely on the local cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 21:33:36 -08:00
parent a63674fe56
commit a02876c20d
3 changed files with 33 additions and 2 deletions

View File

@@ -189,6 +189,29 @@ async def timer_action(
await db.commit() await db.commit()
await db.refresh(session) 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 # Broadcast the timer event to all TV clients
ws_payload = { ws_payload = {
"event": body.event_type, "event": body.event_type,
@@ -196,6 +219,7 @@ async def timer_action(
"block_id": event.block_id, "block_id": event.block_id,
"current_block_id": session.current_block_id, "current_block_id": session.current_block_id,
"is_active": session.is_active, "is_active": session.is_active,
"block_elapsed_seconds": block_elapsed_seconds,
} }
await manager.broadcast(session.child_id, ws_payload) await manager.broadcast(session.child_id, ws_payload)

View File

@@ -86,9 +86,12 @@ export const useScheduleStore = defineStore('schedule', () => {
blockStartedAt.value = null blockStartedAt.value = null
isPaused.value = true 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') { 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() blockStartedAt.value = Date.now()
isPaused.value = false isPaused.value = false
} }

View File

@@ -236,6 +236,10 @@ async function selectBlock(block) {
await scheduleStore.sendTimerAction(scheduleStore.session.id, 'pause', currentId) 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) scheduleStore.sendTimerAction(scheduleStore.session.id, 'start', block.id)
} }