Add per-block agenda overrides for daily sessions

- Add 📝 Agenda button to each block in Today's Schedule on the Dashboard
- Dialog allows setting a free-text activity/note for that block for the current day
- Agenda replaces subject options in the TV center panel while set; clears on session end
- Backend: new SessionBlockAgenda model, PUT /api/sessions/{id}/blocks/{block_id}/agenda
- Agendas included in dashboard snapshot and session_update WS broadcast
- New agenda_update WS event keeps TV in sync live when agenda is saved or cleared
- Update README with feature description, project structure, and WS event table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 07:58:30 -07:00
parent d724262e27
commit fdd85d3df5
9 changed files with 246 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
@@ -12,6 +13,7 @@ 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.models.session_block_agenda import SessionBlockAgenda
from app.models.user import User
from app.schemas.session import DailySessionOut, SessionStart, TimerAction
from sqlalchemy import delete as sql_delete
@@ -85,6 +87,13 @@ async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
)
break_activities = [item.text for item in break_result.scalars().all()]
agendas_result = await db.execute(
select(SessionBlockAgenda).where(SessionBlockAgenda.session_id == session.id)
)
block_agendas = {
str(item.block_id): item.text for item in agendas_result.scalars().all()
}
payload = {
"event": "session_update",
"session": {
@@ -98,6 +107,7 @@ async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
"completed_block_ids": completed_ids,
"morning_routine": morning_routine,
"break_activities": break_activities,
"block_agendas": block_agendas,
}
await manager.broadcast(session.child_id, payload)
@@ -308,3 +318,49 @@ async def timer_action(
await manager.broadcast(session.child_id, ws_payload)
return session
class AgendaUpdate(BaseModel):
text: str
@router.put("/{session_id}/blocks/{block_id}/agenda", status_code=status.HTTP_204_NO_CONTENT)
async def set_block_agenda(
session_id: int,
block_id: int,
body: AgendaUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(DailySession)
.join(Child)
.where(DailySession.id == session_id, Child.user_id == current_user.id)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
existing = await db.execute(
select(SessionBlockAgenda).where(
SessionBlockAgenda.session_id == session_id,
SessionBlockAgenda.block_id == block_id,
)
)
agenda = existing.scalar_one_or_none()
clean_text = body.text.strip()
if clean_text:
if agenda:
agenda.text = clean_text
else:
db.add(SessionBlockAgenda(session_id=session_id, block_id=block_id, text=clean_text))
elif agenda:
await db.delete(agenda)
await db.commit()
await manager.broadcast(session.child_id, {
"event": "agenda_update",
"block_id": block_id,
"text": clean_text,
})