From c05543d855c88ec4677820273b920a6b77bb6250 Mon Sep 17 00:00:00 2001 From: derekc Date: Tue, 3 Mar 2026 13:38:40 -0800 Subject: [PATCH] Rework day progress bar to use block-duration time instead of wall clock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the TV and parent dashboard progress bars now calculate % complete based on total scheduled block time vs. remaining block + break time, so the bar only advances while blocks are actively being worked. TV bar labels changed to "🟢 Start" and "Finish 🏁". Parent dashboard shows first block's scheduled start time on the left and a live estimated finish time (now + remaining block/break time) on the right. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 7 +- frontend/src/views/DashboardView.vue | 110 +++++++++++++++++---------- frontend/src/views/TVView.vue | 28 +++---- 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 04f36e8..1aedefc 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ A self-hosted web app for managing homeschool schedules, tracking daily learning ## Features -- **TV Dashboard** — Full-screen display for the living room TV. Shows the current subject, countdown timer, day progress, activity options, and the schedule block list. Updates live without page refresh via WebSocket. +- **TV Dashboard** — Full-screen display for the living room TV. Shows the current subject, countdown timer, day progress bar, activity options, and the schedule block list. Updates live without page refresh via WebSocket. - **Morning Routine** — Define a list of morning routine items in Admin. They appear in the TV dashboard Activities panel during the "Good Morning" greeting before the first block starts, then switch to subject-specific activities once a block begins. - **Break Time** — Each schedule block can optionally include a break at the end. Enable the checkbox and set a duration (in minutes) when building a block in Admin. Once the block's main timer is done, a **Break Time** section appears on the Dashboard with its own **Start / Pause / Resume / Reset** controls — the break does not start automatically. While break is active the TV left column switches to an amber break badge and countdown timer, and the center column shows the configurable **Break Activities** list instead of subject options. - **Break Activities** — A global list of break-time activities (e.g. "Get a snack", "Go outside") managed in Admin → Break Activities, using the same add/edit/delete interface as Morning Routine. These items are shown on the TV during any active break. -- **Schedule Builder** — Create named schedule templates with time blocks assigned to subjects. Set optional school day start/end hours for a day-progress bar. Each block supports an optional custom duration override, label, and break time setting. Managed inside the Admin page. +- **Day Progress Bar** — Both the TV dashboard and the parent dashboard display a progress bar showing how far through the day the child is. Progress is calculated from total scheduled block time vs. remaining block time — not wall-clock time — so it advances only as blocks are actively worked. On the TV the bar is labeled **🟢 Start** and **Finish 🏁**. On the parent dashboard the left label shows the scheduled start time of the first block and the right label shows a live-updating **estimated finish time** computed as the current time plus all remaining block time and break time for incomplete blocks. +- **Schedule Builder** — Create named schedule templates with time blocks assigned to subjects. Each block supports an optional custom duration override, label, and break time setting. Managed inside the Admin page. - **Daily Sessions** — Start a school day against a schedule template. Click any block in the list to select it as the current block. Use the **Start** button to begin timing, **Pause** to stop, **Resume** to continue from where you left off, and **Reset** to clear the elapsed time and restart the timer from zero. Elapsed time per block is remembered across switches, so returning to a block picks up where it left off. - **Block Timer Remaining** — Each block in the schedule list shows time remaining (allocated duration minus elapsed), counting down live on both the parent dashboard and the TV sidebar. Shows "< 1 min" when under a minute, and "Done!" when the full duration is elapsed. - **Activity Log** — Automatically records every timer event (day started, block start/pause/resume/complete/skip/reset, break start/pause/resume/reset) and every strike change as a timestamped timeline. Includes which schedule template was used. Supports manual notes with free text. Browse and filter history by child and date. @@ -192,7 +193,7 @@ While a session is active, clicking a block in the schedule list **selects** it | URL | Description | |-----|-------------| -| `/tv/:childId` | Full-screen display — greeting + morning routine, current block timer with subject activities, break timer with break activities, day progress, schedule sidebar | +| `/tv/:childId` | Full-screen display — greeting + morning routine, current block timer with subject activities, break timer with break activities, day progress bar (🟢 Start → Finish 🏁), schedule sidebar | Point a browser on the living room TV at `http://your-lan-ip:8054/tv/1`. The page connects via WebSocket and updates automatically when a parent starts/stops/advances the timer from the Dashboard. diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 882a16b..1ea8e37 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -47,16 +47,16 @@
Today's Session
-
+
Active {{ dayProgressPercent }}%
- {{ formatDayTime(scheduleStore.dayStartTime) }} + {{ firstBlockStartTime }} {{ currentTimeDisplay }} - {{ formatDayTime(scheduleStore.dayEndTime) }} + {{ estimatedFinishTime }}
@@ -73,6 +73,32 @@ />
+
+
+ + + + +
+ +
+
Reset
- -
-
- - - - -
- -

No active session.

@@ -221,12 +221,6 @@ const templates = ref([]) const now = ref(new Date()) setInterval(() => { now.value = new Date() }, 1000) -function timeStrToMinutes(str) { - if (!str) return null - const [h, m] = str.split(':').map(Number) - return h * 60 + m -} - function formatDayTime(str) { if (!str) return '' const [h, m] = str.split(':').map(Number) @@ -240,11 +234,17 @@ const currentTimeDisplay = computed(() => ) const dayProgressPercent = computed(() => { - const start = timeStrToMinutes(scheduleStore.dayStartTime) - const end = timeStrToMinutes(scheduleStore.dayEndTime) - if (start === null || end === null || end <= start) return 0 - const nowMin = now.value.getHours() * 60 + now.value.getMinutes() - return Math.max(0, Math.min(100, Math.round((nowMin - start) / (end - start) * 100))) + const allBlocks = scheduleStore.blocks + if (!allBlocks.length) return 0 + const totalSeconds = allBlocks.reduce((sum, b) => sum + (b.duration_minutes || 0) * 60, 0) + if (totalSeconds === 0) return 0 + const remainingSeconds = allBlocks.reduce((sum, b) => { + if (scheduleStore.completedBlockIds.includes(b.id)) return sum + const blockTotal = (b.duration_minutes || 0) * 60 + const elapsed = blockElapsed(b) + return sum + Math.max(0, blockTotal - elapsed) + }, 0) + return Math.max(0, Math.min(100, Math.round((totalSeconds - remainingSeconds) / totalSeconds * 100))) }) let wsDisconnect = null @@ -291,6 +291,36 @@ function blockElapsed(block) { return scheduleStore.blockElapsedCache[block.id] || 0 } +function breakElapsed(block) { + if (scheduleStore.isBreakMode && block.id === scheduleStore.session?.current_block_id && scheduleStore.breakStartedAt) { + return scheduleStore.breakElapsedOffset + Math.floor((now.value - scheduleStore.breakStartedAt) / 1000) + } + return scheduleStore.breakElapsedCache[block.id] || 0 +} + +const firstBlockStartTime = computed(() => { + const first = scheduleStore.blocks[0] + return first?.time_start ? formatDayTime(first.time_start) : '' +}) + +const estimatedFinishTime = computed(() => { + const allBlocks = scheduleStore.blocks + if (!allBlocks.length) return '' + let remainingSeconds = 0 + for (const b of allBlocks) { + if (scheduleStore.completedBlockIds.includes(b.id)) continue + remainingSeconds += Math.max(0, (b.duration_minutes || 0) * 60 - blockElapsed(b)) + if (b.break_time_enabled && b.break_time_minutes) { + remainingSeconds += Math.max(0, b.break_time_minutes * 60 - breakElapsed(b)) + } + } + const finish = new Date(now.value.getTime() + remainingSeconds * 1000) + const h = finish.getHours() + const period = h >= 12 ? 'PM' : 'AM' + const hour = h % 12 || 12 + return `${hour}:${String(finish.getMinutes()).padStart(2, '0')} ${period}` +}) + function selectBlock(block) { if (!scheduleStore.session) return // Clicking the current block does nothing — use Start/Pause/Resume buttons diff --git a/frontend/src/views/TVView.vue b/frontend/src/views/TVView.vue index ca571d3..f93e038 100644 --- a/frontend/src/views/TVView.vue +++ b/frontend/src/views/TVView.vue @@ -17,11 +17,11 @@ -
+
- {{ formatDayTime(scheduleStore.dayStartTime) }} + 🟢 Start {{ dayProgressPercent }}% through the day - {{ formatDayTime(scheduleStore.dayEndTime) }} + Finish 🏁
@@ -167,12 +167,6 @@ const dateDisplay = computed(() => ) // Day progress helpers -function timeStrToMinutes(str) { - if (!str) return null - const [h, m] = str.split(':').map(Number) - return h * 60 + m -} - function formatDayTime(str) { if (!str) return '' const [h, m] = str.split(':').map(Number) @@ -182,11 +176,17 @@ function formatDayTime(str) { } const dayProgressPercent = computed(() => { - const start = timeStrToMinutes(scheduleStore.dayStartTime) - const end = timeStrToMinutes(scheduleStore.dayEndTime) - if (start === null || end === null || end <= start) return 0 - const nowMin = now.value.getHours() * 60 + now.value.getMinutes() - return Math.max(0, Math.min(100, Math.round((nowMin - start) / (end - start) * 100))) + const allBlocks = scheduleStore.blocks + if (!allBlocks.length) return 0 + const totalSeconds = allBlocks.reduce((sum, b) => sum + (b.duration_minutes || 0) * 60, 0) + if (totalSeconds === 0) return 0 + const remainingSeconds = allBlocks.reduce((sum, b) => { + if (scheduleStore.completedBlockIds.includes(b.id)) return sum + const blockTotal = (b.duration_minutes || 0) * 60 + const elapsed = blockElapsed(b) + return sum + Math.max(0, blockTotal - elapsed) + }, 0) + return Math.max(0, Math.min(100, Math.round((totalSeconds - remainingSeconds) / totalSeconds * 100))) }) // Countdown to first block