""" 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, )