ameer 2136286275 add live stress harness, app-level admin login rate limit
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.
2026-05-03 00:23:07 +08:00
2026-05-02 02:54:34 +08:00

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:

  1. apt-installs Caddy + Python venv tooling
  2. Creates a quiz system user (no shell, no SSH)
  3. Clones this repo to /opt/quiz
  4. Builds the venv and installs the app
  5. Generates QUIZ_SECRET_KEY, prompts for QUIZ_ADMIN_PASSWORD
  6. Drops the systemd unit and Caddyfile
  7. Starts both services
  8. 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

  1. Provision Aliyun Intl HK ECS pay-as-you-go (ecs.t6-c2m1.large, Ubuntu 24.04 LTS).
  2. Point DNS A-record quiz.ahkhan.me at the new IP.
  3. SSH in as root, run the curl|bash one-liner above.
  4. Open quiz.ahkhan.me/admin/, log in, upload the week's pool JSON, create a session.
  5. Share the QR / join URL with the class.
  6. After class: scp root@<ip>:/opt/quiz/quiz.db ./backups/quiz-YYYY-MM-DD.db
  7. 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).

Description
No description provided
Readme 226 KiB
Languages
Python 43.7%
JavaScript 33%
CSS 19.3%
Shell 3.1%
Smarty 0.6%
Other 0.3%