Tie break timer and main block timer together

- Starting break now pauses the main block timer (frontend optimistic +
  backend implicit pause event recorded before break_start)
- Resuming/starting the main block while break is active pauses the
  break timer and exits break mode on all clients including TV
- Timer display counts negative past zero so overtime is visible while
  label stays "Done!"
- Fixed WS start handler incorrectly skipping break-mode clear when
  restarting the same block; resume handler now also clears break mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 14:24:50 -08:00
parent c565c94a23
commit a8e1b322f1
4 changed files with 73 additions and 16 deletions

View File

@@ -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)
})

View File

@@ -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,

View File

@@ -88,7 +88,7 @@
<button
class="btn-sm"
v-if="scheduleStore.isPaused && scheduleStore.blockElapsedOffset > 0"
@click="sendAction('resume')"
@click="scheduleStore.resumeCurrentBlock(scheduleStore.session.id)"
>Resume</button>
<button
class="btn-sm"