Files
homeschool/backend/app/routers/dashboard.py
derekc 7346159745 Sort schedule blocks by start time instead of order_index
Backend queries and model relationship now order by time_start.
Frontend also sorts blocks client-side for reliability across all views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 16:22:17 -08:00

106 lines
3.8 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.subject import Subject # noqa: F401 — needed for selectinload chain
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)
.options(selectinload(ScheduleBlock.subject).selectinload(Subject.options))
.order_by(ScheduleBlock.time_start)
)
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,
)