diff --git a/Release-Notes/v1.1.md b/Release-Notes/v1.1.md index 836d3ef..40d5101 100644 --- a/Release-Notes/v1.1.md +++ b/Release-Notes/v1.1.md @@ -7,7 +7,7 @@ Maintenance release from the 2026-09-24 review (`reports/maintenance-2026-09-24. ### Security - Rate limits now apply to each client. nginx resolves the real client IP through Cloudflare → NPM (`set_real_ip_from` + `real_ip_recursive`). Before this, every request looked like it came from NPM, so all users shared one login limit. - ntfy alerts show the real client IP (`X-Real-IP` from nginx). They no longer use the first `X-Forwarded-For` entry, which the client controls. -- Python dependency updates: +- Python dependency updates (pydantic now pinned at 2.13.5): - PyJWT 2.15.0 (fixes an auth bypass) - anyio 4.15.1 - fastapi 0.141.1 / starlette 1.7.0 @@ -29,6 +29,7 @@ Maintenance release from the 2026-09-24 review (`reports/maintenance-2026-09-24. - Docker log rotation on all services: json-file, 10 MB × 3. ### Fixes +- **Timer actions returned 500** after the FastAPI 0.141 upgrade. The session queries didn't load `Subject.options`, which `DailySessionOut` serializes. Old FastAPI silently swallowed the failed lazy-load; the new one raises it. Now eager-loaded in `sessions.py` (get/timer) and `dashboard.py`. - The meeting alert catch-up window fix (`8e92ae6`) is now deployed. ### Known / accepted diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index f3fd44e..ef234d6 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -39,7 +39,7 @@ async def get_dashboard(tv_token: int, db: AsyncSession = Depends(get_db)): DailySession.session_date == date.today(), DailySession.is_active == True, ) - .options(selectinload(DailySession.current_block)) + .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject).selectinload(Subject.options)) .limit(1) ) session = session_result.scalar_one_or_none() diff --git a/backend/app/routers/sessions.py b/backend/app/routers/sessions.py index b5fe401..22bf73b 100644 --- a/backend/app/routers/sessions.py +++ b/backend/app/routers/sessions.py @@ -172,7 +172,7 @@ async def get_session( select(DailySession) .join(Child) .where(DailySession.id == session_id, Child.user_id == current_user.id) - .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject)) + .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject).selectinload(Subject.options)) ) session = result.scalar_one_or_none() if not session: @@ -191,7 +191,7 @@ async def timer_action( select(DailySession) .join(Child) .where(DailySession.id == session_id, Child.user_id == current_user.id) - .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject)) + .options(selectinload(DailySession.current_block).selectinload(ScheduleBlock.subject).selectinload(Subject.options)) ) session = result.scalar_one_or_none() if not session: diff --git a/backend/requirements.txt b/backend/requirements.txt index d0f6eda..6c3cecc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,6 +9,7 @@ PyJWT==2.15.0 cryptography==50.0.1 passlib[bcrypt]==1.7.4 bcrypt==3.2.2 +pydantic==2.13.5 pydantic-settings==2.5.2 alembic==1.20.0 Mako==1.4.3 diff --git a/reports/maintenance-2026-09-24.md b/reports/maintenance-2026-09-24.md index 5ca66da..41bb211 100644 --- a/reports/maintenance-2026-09-24.md +++ b/reports/maintenance-2026-09-24.md @@ -156,6 +156,10 @@ This section was first blocked on backups. The user then asked for the HIGH find | homeschool_backend | homeschool-backend | sha256:51b49afb4ec4… | | homeschool_frontend | homeschool-frontend | sha256:f9a737699cda… | +### Post-deploy regression + +After the HIGH deploy, `POST /api/sessions/{id}/timer` returned **500 on every call**: 8 failures, 0 successes, starting 06:43 UTC. FastAPI 0.141 raises on a failed lazy-load of `current_block.subject.options` during response serialization, where 0.115 silently skipped it. The first smoke test didn't cover timer actions. Fixed in v1.1.0 by eager-loading `Subject.options` in `sessions.py` and `dashboard.py`. The smoke test now covers the full timer lifecycle, the TV dashboard with an active block, schedule/subject writes, and every parameterless GET route (from the OpenAPI spec). + ### Post-update scan (trivy HIGH/CRITICAL) | Image | Before | After |