""" 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.morning_routine import MorningRoutineItem 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 and paused state for the current block from timer_events is_paused = False 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) # Paused if the last tick event was a pause (last_start is None but events exist) is_paused = bool(tick_events) and tick_events[-1].event_type == "pause" routine_result = await db.execute( select(MorningRoutineItem) .where(MorningRoutineItem.user_id == child.user_id) .order_by(MorningRoutineItem.order_index, MorningRoutineItem.id) ) morning_routine = [item.text for item in routine_result.scalars().all()] return DashboardSnapshot( session=session, child=child, blocks=blocks, completed_block_ids=completed_ids, block_elapsed_seconds=block_elapsed_seconds, is_paused=is_paused, day_start_time=day_start_time, day_end_time=day_end_time, morning_routine=morning_routine, )