Study portal: mobile quiz UI, browser speech synthesis, and a team-training admin view

2026-04-01

Two PRs ago the question bank got fixed and expanded to 105 questions. This PR gives the question bank an actual interface — a mobile-first quiz UI designed to be usable during a commute, in a coffee line, wherever. It also adds an admin view so I can track multiple future CAM hires through the same study material.

Mobile-first by design

  • Big tap targets, thumb-reachable answer buttons, one question per screen.
  • HTMX partial swaps for every state transition (next question, check answer, show explanation). No full page reloads, no SPA framework, zero custom JavaScript outside the audio layer.
  • Progress bar that fills as the session advances.
  • Keyboard shortcuts (A / B / C / D) for desktop review sessions.
  • Lives under /study/ with its own router, mounted alongside the owner portal but namespace-isolated so the routes can't collide.

Audio mode, 100% client-side

There's a floating action button that toggles "audio mode." When it's on, the page uses the browser's SpeechSynthesis Web API to read the question stem and its four answers aloud, and then the explanation after the answer is revealed. The user preference persists in localStorage, so toggling once sticks across sessions.

Why this matters: zero API cost and zero network dependency. Text-to-speech happens entirely in the browser, using voices the device already ships with. Works offline. Works on a subway. Works on a dollar-store Android. The alternative would have been piping text through an ElevenLabs or Polly endpoint on every question, which is a cost and latency budget I don't want to take on for a feature that's "nice to have during a drive."

Session state in signed cookies, not on the server

Quiz session state (current question index, which answers they've gotten right, the random seed for the question order) lives in a signed cookie via itsdangerous. This is the same pattern the owner portal uses for auth sessions, and it means:

  • No server-side session store.
  • Stateless handlers — every route is a pure function of (request, cookie).
  • Horizontally scalable by construction.

Per-user progress across sessions — which questions they've seen, how many they got right, weak topics — is stored as JSON files in data/study-progress/, keyed by email. Atomic writes (temp file + rename). No database needed at this scale.

The admin view

/study/admin?key=… gates on a shared admin key (env var) and shows a dashboard of every trainee: total accuracy, weak topics, questions answered, exam readiness score. The use case is training future CAMs who join the company — the admin view lets me see at a glance who's prepared and who needs another week on condo-specific rules.

Files

FilePurpose
portal/study_routes.py11 endpoints (landing, quiz flow, progress, admin)
portal/study_models.pyStudyUser, StudySession, TraineeInfo dataclasses
portal/templates/study/8 templates (mobile CSS, HTMX partials, audio JS)
tests/test_study_portal.py23 tests covering the full quiz lifecycle
docs/feature-study-portal.mdArchitecture decisions + expansion guide

Verification

  • 23 study portal tests passing (quiz start, answer submission with correct/incorrect feedback, explanation rendering, results page, progress tracking across sessions, admin view).
  • 37 study engine tests (question validation, scoring, topic rollups) — 60 total for the study subsystem.
  • Ruff clean.
  • Manual audio mode test on Mobile Safari / Chrome is still on the checklist — SpeechSynthesis has some vendor quirks I want to eyeball on a real device before turning the FAB on by default.

PR: https://github.com/StevieIsmagic/honest-cam/pull/7