Portal Username Matching — Case-Insensitive (2026-07-31)

Problem: Portal usernames are email addresses, but every portal_users lookup was exact case-sensitive match. Phone keyboards autocapitalize the first letter of the login field, so “Julia.marketis@jeswork.com” + correct password returned 401. Bit Julia Marketis (JES Baltimore scheduler) on 2026-07-31.

Fix (entirely-api 03e7f9c): services/portal_auth.py gains _normalize_username() (strip + lowercase), applied at the top of every service function that takes a username: authenticate, get_user, create_user, update_user, set_active, reset_password, change_password, delete_user, touch_session. Routes needed no changes — all portal_users writes go through the service, and JWT sub carries the DB-stored (lowercase) username.

Why no COLLATE NOCASE / migration: live DB verified zero rows where username != trim(lower(username)), so stored data was already normalized. create_user now stores lowercase, so the case-sensitive UNIQUE constraint cannot admit case-variant duplicates going forward.

Tests: tests/test_portal_auth_username_case.py (10 tests — autocap variant, whitespace, case-variant duplicate rejection, wrong-password still 401). Suite 313 passed host-side (test_cert_pipeline pillow_heif collection skip pre-existing).

Verified live: API rebuilt, worker/scheduler restarted; login with “SCHEDULER1” and ” Scheduler1 ” → 200 with normalized username in response; wrong password → 401.