""" Public dashboard endpoint — no authentication required. Used by the TV view to get the initial session snapshot before WebSocket connects. """ from datetime import date 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.break_activity import BreakActivityItem from app.models.schedule import ScheduleBlock 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 from app.utils.timer import compute_block_elapsed, compute_break_elapsed 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 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() 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 is_break_active = False break_elapsed_seconds = 0 is_break_paused = False if session and session.current_block_id: block_elapsed_seconds, is_paused = await compute_block_elapsed( db, session.id, session.current_block_id ) # Determine if break mode is active: check whether the most recent # timer event for this block (main or break) is a break event. last_event_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", "break_start", "break_resume", "break_pause", "break_reset", ]), ) .order_by(TimerEvent.occurred_at.desc()) .limit(1) ) last_event = last_event_result.scalar_one_or_none() if last_event and last_event.event_type.startswith("break_"): is_break_active = True break_elapsed_seconds, is_break_paused = await compute_break_elapsed( db, session.id, session.current_block_id ) 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()] break_result = await db.execute( select(BreakActivityItem) .where(BreakActivityItem.user_id == child.user_id) .order_by(BreakActivityItem.order_index, BreakActivityItem.id) ) break_activities = [item.text for item in break_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, morning_routine=morning_routine, break_activities=break_activities, is_break_active=is_break_active, break_elapsed_seconds=break_elapsed_seconds, is_break_paused=is_break_paused, )