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:
@@ -191,18 +191,30 @@ async def timer_action(
|
|||||||
BREAK_EVENTS = {"break_start", "break_pause", "break_resume", "break_reset"}
|
BREAK_EVENTS = {"break_start", "break_pause", "break_resume", "break_reset"}
|
||||||
if body.event_type in BREAK_EVENTS:
|
if body.event_type in BREAK_EVENTS:
|
||||||
block_id = body.block_id or session.current_block_id
|
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,
|
session_id=session.id,
|
||||||
block_id=block_id,
|
block_id=block_id,
|
||||||
event_type=body.event_type,
|
event_type=body.event_type,
|
||||||
)
|
))
|
||||||
db.add(event)
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(session)
|
await db.refresh(session)
|
||||||
|
|
||||||
break_elapsed_seconds = 0
|
break_elapsed_seconds = 0
|
||||||
|
block_elapsed_seconds = 0
|
||||||
if body.event_type in ("break_start", "break_reset") and block_id:
|
if body.event_type in ("break_start", "break_reset") and block_id:
|
||||||
break_elapsed_seconds, _ = await compute_break_elapsed(db, session.id, 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 = {
|
ws_payload = {
|
||||||
"event": body.event_type,
|
"event": body.event_type,
|
||||||
@@ -211,6 +223,7 @@ async def timer_action(
|
|||||||
"current_block_id": session.current_block_id,
|
"current_block_id": session.current_block_id,
|
||||||
"is_active": session.is_active,
|
"is_active": session.is_active,
|
||||||
"break_elapsed_seconds": break_elapsed_seconds,
|
"break_elapsed_seconds": break_elapsed_seconds,
|
||||||
|
"block_elapsed_seconds": block_elapsed_seconds,
|
||||||
}
|
}
|
||||||
await manager.broadcast(session.child_id, ws_payload)
|
await manager.broadcast(session.child_id, ws_payload)
|
||||||
return session
|
return session
|
||||||
|
|||||||
@@ -58,10 +58,12 @@ const elapsed = computed(() => {
|
|||||||
const remaining = computed(() => Math.max(0, blockDuration.value - elapsed.value))
|
const remaining = computed(() => Math.max(0, blockDuration.value - elapsed.value))
|
||||||
|
|
||||||
const display = computed(() => {
|
const display = computed(() => {
|
||||||
const s = remaining.value
|
const s = blockDuration.value - elapsed.value
|
||||||
const m = Math.floor(s / 60)
|
if (s < 0) {
|
||||||
const sec = s % 60
|
const abs = Math.abs(s)
|
||||||
return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
|
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(() => {
|
const label = computed(() => {
|
||||||
@@ -72,7 +74,7 @@ const label = computed(() => {
|
|||||||
const CIRCUMFERENCE = 2 * Math.PI * 88
|
const CIRCUMFERENCE = 2 * Math.PI * 88
|
||||||
const dashOffset = computed(() => {
|
const dashOffset = computed(() => {
|
||||||
if (!blockDuration.value) return CIRCUMFERENCE
|
if (!blockDuration.value) return CIRCUMFERENCE
|
||||||
const pct = remaining.value / blockDuration.value
|
const pct = Math.max(0, remaining.value / blockDuration.value)
|
||||||
return CIRCUMFERENCE * (1 - pct)
|
return CIRCUMFERENCE * (1 - pct)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
if (event.block_id) breakElapsedCache.value[event.block_id] = elapsed
|
if (event.block_id) breakElapsedCache.value[event.block_id] = elapsed
|
||||||
breakStartedAt.value = Date.now()
|
breakStartedAt.value = Date.now()
|
||||||
isBreakMode.value = true
|
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 (event.event === 'break_pause') {
|
||||||
if (breakStartedAt.value) {
|
if (breakStartedAt.value) {
|
||||||
@@ -140,13 +146,10 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
}
|
}
|
||||||
blockStartedAt.value = Date.now()
|
blockStartedAt.value = Date.now()
|
||||||
isPaused.value = false
|
isPaused.value = false
|
||||||
// Switching to a new block clears break mode
|
|
||||||
if (event.block_id !== event.current_block_id || !isBreakMode.value) {
|
|
||||||
isBreakMode.value = false
|
isBreakMode.value = false
|
||||||
breakStartedAt.value = null
|
breakStartedAt.value = null
|
||||||
breakElapsedOffset.value = breakElapsedCache.value[event.block_id] ?? 0
|
breakElapsedOffset.value = breakElapsedCache.value[event.block_id] ?? 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Reset — clear elapsed to 0 and start counting immediately
|
// Reset — clear elapsed to 0 and start counting immediately
|
||||||
if (event.event === 'reset') {
|
if (event.event === 'reset') {
|
||||||
if (event.block_id) blockElapsedCache.value[event.block_id] = 0
|
if (event.block_id) blockElapsedCache.value[event.block_id] = 0
|
||||||
@@ -175,6 +178,8 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
if (event.event === 'resume') {
|
if (event.event === 'resume') {
|
||||||
blockStartedAt.value = Date.now()
|
blockStartedAt.value = Date.now()
|
||||||
isPaused.value = false
|
isPaused.value = false
|
||||||
|
isBreakMode.value = false
|
||||||
|
breakStartedAt.value = null
|
||||||
}
|
}
|
||||||
// Timer events update session state
|
// Timer events update session state
|
||||||
if (event.current_block_id !== undefined && session.value) {
|
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).
|
// Start the timer for the currently selected block (optimistic).
|
||||||
function startCurrentBlock(sessionId) {
|
function startCurrentBlock(sessionId) {
|
||||||
if (!session.value?.current_block_id) return
|
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
|
isPaused.value = false
|
||||||
blockStartedAt.value = Date.now()
|
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
|
// Break timer actions
|
||||||
function startBreak(sessionId) {
|
function startBreak(sessionId) {
|
||||||
if (!session.value?.current_block_id) return
|
if (!session.value?.current_block_id) return
|
||||||
const blockId = session.value.current_block_id
|
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
|
isBreakMode.value = true
|
||||||
breakElapsedOffset.value = breakElapsedCache.value[blockId] ?? 0
|
breakElapsedOffset.value = breakElapsedCache.value[blockId] ?? 0
|
||||||
breakStartedAt.value = Date.now()
|
breakStartedAt.value = Date.now()
|
||||||
@@ -332,6 +373,7 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
switchBlock,
|
switchBlock,
|
||||||
selectBlock,
|
selectBlock,
|
||||||
startCurrentBlock,
|
startCurrentBlock,
|
||||||
|
resumeCurrentBlock,
|
||||||
resetCurrentBlock,
|
resetCurrentBlock,
|
||||||
startBreak,
|
startBreak,
|
||||||
pauseBreak,
|
pauseBreak,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<button
|
<button
|
||||||
class="btn-sm"
|
class="btn-sm"
|
||||||
v-if="scheduleStore.isPaused && scheduleStore.blockElapsedOffset > 0"
|
v-if="scheduleStore.isPaused && scheduleStore.blockElapsedOffset > 0"
|
||||||
@click="sendAction('resume')"
|
@click="scheduleStore.resumeCurrentBlock(scheduleStore.session.id)"
|
||||||
>Resume</button>
|
>Resume</button>
|
||||||
<button
|
<button
|
||||||
class="btn-sm"
|
class="btn-sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user