diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 5d7b348..d19f4f0 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -2,7 +2,7 @@ Public dashboard endpoint — no authentication required. Used by the TV view to get the initial session snapshot before WebSocket connects. """ -from datetime import date +from datetime import date, datetime from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession @@ -40,6 +40,7 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)): blocks = [] completed_ids = [] + block_elapsed_seconds = 0 if session and session.template_id: blocks_result = await db.execute( @@ -57,9 +58,34 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)): ) completed_ids = [e.block_id for e in events_result.scalars().all() if e.block_id] + # Compute elapsed seconds for the current block from timer_events + if session and session.current_block_id: + tick_result = await db.execute( + select(TimerEvent) + .where( + TimerEvent.session_id == session.id, + TimerEvent.block_id == session.current_block_id, + TimerEvent.event_type.in_(["start", "resume", "pause"]), + ) + .order_by(TimerEvent.occurred_at) + ) + tick_events = tick_result.scalars().all() + last_start = None + elapsed = 0.0 + for e in tick_events: + if e.event_type in ("start", "resume"): + last_start = e.occurred_at + elif e.event_type == "pause" and last_start: + elapsed += (e.occurred_at - last_start).total_seconds() + last_start = None + if last_start: + elapsed += (datetime.utcnow() - last_start).total_seconds() + block_elapsed_seconds = int(elapsed) + return DashboardSnapshot( session=session, child=child, blocks=blocks, completed_block_ids=completed_ids, + block_elapsed_seconds=block_elapsed_seconds, ) diff --git a/backend/app/schemas/session.py b/backend/app/schemas/session.py index e0c6318..5a3aca9 100644 --- a/backend/app/schemas/session.py +++ b/backend/app/schemas/session.py @@ -42,3 +42,4 @@ class DashboardSnapshot(BaseModel): child: ChildOut blocks: list[ScheduleBlockOut] = [] completed_block_ids: list[int] = [] + block_elapsed_seconds: int = 0 # seconds already elapsed for the current block diff --git a/frontend/src/components/TimerDisplay.vue b/frontend/src/components/TimerDisplay.vue index 93f9af3..3c3934a 100644 --- a/frontend/src/components/TimerDisplay.vue +++ b/frontend/src/components/TimerDisplay.vue @@ -1,5 +1,9 @@ diff --git a/frontend/src/stores/schedule.js b/frontend/src/stores/schedule.js index 838de7b..8055a8a 100644 --- a/frontend/src/stores/schedule.js +++ b/frontend/src/stores/schedule.js @@ -7,6 +7,9 @@ export const useScheduleStore = defineStore('schedule', () => { const blocks = ref([]) const completedBlockIds = ref([]) const child = ref(null) + const isPaused = ref(false) + const blockStartedAt = ref(null) // Date.now() ms when current counting period started + const blockElapsedOffset = ref(0) // seconds already elapsed before blockStartedAt const currentBlock = computed(() => session.value?.current_block_id @@ -23,7 +26,17 @@ export const useScheduleStore = defineStore('schedule', () => { session.value = snapshot.session blocks.value = snapshot.blocks || [] completedBlockIds.value = snapshot.completed_block_ids || [] + isPaused.value = false if (snapshot.child) child.value = snapshot.child + // Restore elapsed time from server-computed value + const serverElapsed = snapshot.block_elapsed_seconds || 0 + if (snapshot.session?.current_block_id && serverElapsed > 0) { + blockElapsedOffset.value = serverElapsed + blockStartedAt.value = Date.now() + } else { + blockElapsedOffset.value = 0 + blockStartedAt.value = null + } } function applyWsEvent(event) { @@ -34,8 +47,30 @@ export const useScheduleStore = defineStore('schedule', () => { // Session ended if (event.is_active === false) { session.value = null + isPaused.value = false + blockStartedAt.value = null + blockElapsedOffset.value = 0 return } + // Pause — accumulate elapsed, stop counting + if (event.event === 'pause') { + if (blockStartedAt.value) { + blockElapsedOffset.value += Math.floor((Date.now() - blockStartedAt.value) / 1000) + } + blockStartedAt.value = null + isPaused.value = true + } + // Start (new block) — reset elapsed, begin counting + if (event.event === 'start') { + blockElapsedOffset.value = 0 + blockStartedAt.value = Date.now() + isPaused.value = false + } + // Resume — continue from where we left off + if (event.event === 'resume') { + blockStartedAt.value = Date.now() + isPaused.value = false + } // Timer events update session state if (event.current_block_id !== undefined && session.value) { session.value.current_block_id = event.current_block_id @@ -73,6 +108,9 @@ export const useScheduleStore = defineStore('schedule', () => { blocks, completedBlockIds, child, + isPaused, + blockStartedAt, + blockElapsedOffset, currentBlock, progressPercent, applySnapshot, diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 4005498..513fdbe 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -21,13 +21,27 @@ {{ scheduleStore.progressPercent }}% complete +
+ +
- +
@@ -96,6 +110,7 @@ import NavBar from '@/components/NavBar.vue' import ChildSelector from '@/components/ChildSelector.vue' import ProgressBar from '@/components/ProgressBar.vue' import ScheduleBlock from '@/components/ScheduleBlock.vue' +import TimerDisplay from '@/components/TimerDisplay.vue' const childrenStore = useChildrenStore() const scheduleStore = useScheduleStore() @@ -135,6 +150,8 @@ async function sendAction(type) { function selectBlock(block) { if (!scheduleStore.session) return + scheduleStore.session.current_block_id = block.id + scheduleStore.isPaused = false scheduleStore.sendTimerAction(scheduleStore.session.id, 'start', block.id) } @@ -189,6 +206,7 @@ h1 { font-size: 1.75rem; font-weight: 700; } font-weight: 600; } +.current-block-timer { display: flex; justify-content: center; margin: 1rem 0; } .session-actions { display: flex; gap: 0.5rem; margin-top: 1rem; flex-wrap: wrap; } .btn-sm { diff --git a/frontend/src/views/TVView.vue b/frontend/src/views/TVView.vue index b6d1911..604f069 100644 --- a/frontend/src/views/TVView.vue +++ b/frontend/src/views/TVView.vue @@ -26,6 +26,9 @@
{{ scheduleStore.currentBlock.notes }}