Files
homeschool/backend/app/routers/dashboard.py
derekc 3e7ff2a50b 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>
2026-02-28 10:08:07 -08:00

104 lines
3.7 KiB
Python

"""
Public dashboard endpoint — no authentication required.
Used by the TV view to get the initial session snapshot before WebSocket connects.
"""
from datetime import date, datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.dependencies import get_db
from app.models.child import Child
from app.models.schedule import ScheduleBlock, ScheduleTemplate
from app.models.session import DailySession, TimerEvent
from app.schemas.session import DashboardSnapshot
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
@router.get("/{child_id}", response_model=DashboardSnapshot)
async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
child_result = await db.execute(select(Child).where(Child.id == child_id, Child.is_active == True))
child = child_result.scalar_one_or_none()
if not child:
raise HTTPException(status_code=404, detail="Child not found")
# Get today's active session
session_result = await db.execute(
select(DailySession)
.where(
DailySession.child_id == child_id,
DailySession.session_date == date.today(),
DailySession.is_active == True,
)
.options(selectinload(DailySession.current_block))
.limit(1)
)
session = session_result.scalar_one_or_none()
blocks = []
completed_ids = []
block_elapsed_seconds = 0
day_start_time = None
day_end_time = None
if session and session.template_id:
blocks_result = await db.execute(
select(ScheduleBlock)
.where(ScheduleBlock.template_id == session.template_id)
.order_by(ScheduleBlock.order_index)
)
blocks = blocks_result.scalars().all()
template_result = await db.execute(
select(ScheduleTemplate).where(ScheduleTemplate.id == session.template_id)
)
template = template_result.scalar_one_or_none()
if template:
day_start_time = template.day_start_time
day_end_time = template.day_end_time
events_result = await db.execute(
select(TimerEvent).where(
TimerEvent.session_id == session.id,
TimerEvent.event_type == "complete",
)
)
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,
day_start_time=day_start_time,
day_end_time=day_end_time,
)