Add time-based day progress bar to dashboards

Replaces block-count progress with a wall-clock progress bar driven by
configurable day start/end hours on each schedule template.

- ScheduleTemplate: add day_start_time / day_end_time (TIME, nullable)
- Startup migration: idempotent ALTER TABLE for existing DBs
- Dashboard snapshot: includes day_start_time / day_end_time from template
- Admin → Schedules: time pickers in block editor to set day hours
- Dashboard view: time-based progress bar with start/current/end labels
- TV view: full-width day progress strip between header and main content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 10:08:07 -08:00
parent 462205cdc1
commit 3e7ff2a50b
10 changed files with 213 additions and 38 deletions

View File

@@ -16,11 +16,21 @@
<div class="card session-card">
<div class="card-title">Today's Session</div>
<div v-if="scheduleStore.session">
<div class="session-info">
<span class="badge-active">Active</span>
<span>{{ scheduleStore.progressPercent }}% complete</span>
<div v-if="scheduleStore.dayStartTime && scheduleStore.dayEndTime" class="day-progress-section">
<div class="day-progress-header">
<span class="badge-active">Active</span>
<span class="day-progress-pct">{{ dayProgressPercent }}%</span>
</div>
<ProgressBar :percent="dayProgressPercent" />
<div class="day-progress-times">
<span>{{ formatDayTime(scheduleStore.dayStartTime) }}</span>
<span>{{ currentTimeDisplay }}</span>
<span>{{ formatDayTime(scheduleStore.dayEndTime) }}</span>
</div>
</div>
<div v-else class="session-info">
<span class="badge-active">Active</span>
</div>
<ProgressBar :percent="scheduleStore.progressPercent" />
<div v-if="scheduleStore.currentBlock" class="current-block-timer">
<TimerDisplay
compact
@@ -119,6 +129,36 @@ const showStartDialog = ref(false)
const selectedTemplate = ref(null)
const templates = ref([])
// Day progress clock (minute precision is enough)
const now = ref(new Date())
setInterval(() => { now.value = new Date() }, 60000)
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)
const period = h >= 12 ? 'PM' : 'AM'
const hour = h % 12 || 12
return `${hour}:${String(m).padStart(2, '0')} ${period}`
}
const currentTimeDisplay = computed(() =>
now.value.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
)
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)))
})
let wsDisconnect = null
async function loadDashboard() {
@@ -197,6 +237,23 @@ h1 { font-size: 1.75rem; font-weight: 700; }
color: #94a3b8;
}
.day-progress-section { margin-bottom: 0.75rem; }
.day-progress-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.day-progress-pct { font-size: 0.9rem; color: #94a3b8; }
.day-progress-times {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: #475569;
margin-top: 0.35rem;
font-variant-numeric: tabular-nums;
}
.badge-active {
background: #14532d;
color: #4ade80;