Root causes:
1. joinedload(Quiz.questions) was SLOWER (129ms vs 75ms) with the custom
secondary relationship — removed, back to lazy load
2. Redis progress could have stale attempt_id from a completed submission:
GET /attempts/progress now validates the attempt exists and is not completed
before returning; stale entries are auto-deleted
POST /attempts/{id}/submit now clears Redis progress server-side on submit
3. Navbar jobs polling was running every 4s even with no active jobs,
causing unnecessary requests — now polls every 30s when idle,
every 3s when jobs are actively running
Also cleared 3 stale Redis progress entries that were pointing to
already-completed attempts (which caused silent submit failures).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>