Three small UX/fairness tweaks from manual live testing: 1. Post-submit "wait for reveal" screen: show only the response time, no score. The +score reveal leaked correctness — any positive number = correct, zero = wrong — short-circuiting the "stop and think" beat the reveal pause was supposed to enforce. Time stays as the engagement signal; score now waits for the instructor reveal. 2. Final-screen "Correct X / Y" denominator is now total_questions instead of questions_answered. Missed questions are scored zero, so they belong in the denominator visibly. Server adds total_questions to the session_ended payload. 3. Projector score-distribution: drop the in-chart count labels (they collided with each other and with the median tag at small N), restore the previously-computed-but-not-rendered x-axis tick labels at the bottom. Stats line at the foot keeps n / mean / max. Also: short-circuit the per-submit instructor + presence broadcasts when no instructor / projector is connected (no listener, no DB work). The 50-student load test was tight on margin against its 2 s time_limit; with the new presence_message / live_histogram_message DB queries firing on every submit, the margin disappeared on busy boxes. Conftest fixture also bumped to 8 s per question for the same reason — gives breathing room for sequential WS submits in the load test. 71/71 pytest green.
Live in-lecture quiz portal
FastAPI + WebSocket + SQLite quiz portal designed for ~40 students per class session. Single-process, in-memory room manager, vanilla HTML/JS front-end, Caddy in front for TLS.
Quick local run
python3 -m venv .venv
. .venv/bin/activate
pip install -e '.[dev]'
cp .env.example .env # edit QUIZ_SECRET_KEY + QUIZ_ADMIN_PASSWORD
uvicorn app.main:app --host 127.0.0.1 --port 8001 --reload
Open http://127.0.0.1:8001/admin/, log in, create a quiz pool from a
JSON pool file (see examples/pool_example.json for the schema), create
a session, and share the join URL.
VPS deploy (one-shot)
On a fresh Ubuntu 24.04 LTS root SSH:
curl -fsSL https://gitea.ahkhan.me/apps/quiz/raw/branch/master/deploy/bootstrap.sh | bash
The bootstrap:
- apt-installs Caddy + Python venv tooling
- Creates a
quizsystem user (no shell, no SSH) - Clones this repo to
/opt/quiz - Builds the venv and installs the app
- Generates
QUIZ_SECRET_KEY, prompts forQUIZ_ADMIN_PASSWORD - Drops the systemd unit and Caddyfile
- Starts both services
- Curl-checks
127.0.0.1:8001/healthz
After: quiz.ahkhan.me is live with auto-Let's-Encrypt cert. To override
the domain or repo URL, set DOMAIN= or REPO_URL= in the environment
before running the script.
Class-day workflow
- Provision Aliyun Intl HK ECS pay-as-you-go (
ecs.t6-c2m1.large, Ubuntu 24.04 LTS). - Point DNS A-record
quiz.ahkhan.meat the new IP. - SSH in as root, run the curl|bash one-liner above.
- Open
quiz.ahkhan.me/admin/, log in, upload the week's pool JSON, create a session. - Share the QR / join URL with the class.
- After class:
scp root@<ip>:/opt/quiz/quiz.db ./backups/quiz-YYYY-MM-DD.db - Destroy the instance.
Quiz pool files
Real pool JSON files contain answer keys and must not be committed
to this repo. .gitignore excludes examples/*_pool.json (only
examples/pool_example.json may be tracked). Author pools elsewhere
(e.g., your course-material directory) and upload at runtime via the
admin UI.
Tests
pytest -q
pytest --cov=app
For the WebSocket adversarial stress harness (Node.js + Playwright,
runs in a tmux loop), see tests/stress/README.md.
Spec
SPEC.md documents the locked v1.0 design (state machine, scoring,
identity flow, all WS message types).