tests/stress/live_accuracy.mjs: classroom-scale accuracy + latency test that targets the deployed server (single-session, sid=main). Logs in as admin via /admin/login, resets the session, joins N students serially over HTTP, opens N student WebSockets in batches of 8 (250ms apart) plus the instructor WS, then drives every question through the admin "next" command. Each student picks uniformly random A-D, sends the submit, waits for the submit_ack, and records the round-trip latency. After session_ended, the script verifies that every student whose pick == correct got score > 0, every other submission got score == 0, and reports p50/p95/p99 ack latency. First live run: 50 students, 100 submits, 100% acks, 100% accuracy match, p99 555ms (≈intercontinental RTT to HK). tests/stress/live_loop.sh: tmux-friendly loop that runs the live test every 60s and appends a JSONL summary line per cycle to runs/live_summary.jsonl. Mirrors the morning's api_stress run_loop shape so per-cycle aggregates are easy to scrape. app/rate_limit.py: tiny in-memory token bucket. Capacity + refill in tokens/minute, keyed by client IP via X-Forwarded-For (with a fallback to request.client.host). Process-local state — admin login is the only user. POST /admin/login: rate-limited at 10 attempts/minute/IP. Generous for the legit instructor (who succeeds in 1-2 tries) and prohibitive for brute force from a single attacker IP. Student endpoints deliberately NOT rate-limited because campus students share NAT gateways and IP-level limits would false-positive a whole class. The bucket is per-app-instance (instantiated inside the router factory), so test apps each get a fresh one and tests don't poison each other.
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).