ameer 168cffea8b feat(options): letterless student/projector UI + text-on-wire submit
Both student-facing surfaces (the per-device join page and the
front-of-room projector) now render options as text only, no A/B/C/D
chips, no numeric prefixes. The letter-namespace remains internal:
canonical A..D in pool.json + submissions storage; canonical position
1..4 in the CSV export. Admin dashboard keeps letters because it is the
instructor's private console.

Why this lands now: the discussion of per-student option shuffling
flagged that A/B/C/D as discussion handles ("the answer is B") is itself
a leaky channel. Removing the labels closes that channel for the
non-shuffled case and keeps it closed if shuffling is added later.

Wire protocol: the submit message carries the option's full text
("answer": "Pipelining"). Server's submit_answer resolves text -> letter
via app.pool.resolve_option_key, which also accepts a canonical letter
so internal callers and tests stay readable. A non-matching string is
recorded as a zero-score submission with answer=NULL, locked in via the
PK + existing_submit_ack short-circuit. So an attempted UI bypass that
posts a fabricated string just produces a wrong answer; no retry.

CSV: A=1 .. D=2 .. C=3 .. D=4 in the answer column. Empty when no option
matched. Header unchanged so downstream pandas readers don't break, but
the value type is int instead of letter.

Histogram: the failsafe "submitted-but-no-match" row buckets into
"missed" alongside genuine misses — both yield zero credit and the
instructor cares about the same thing.

Tests:
- test_submit_accepts_option_text_resolves_to_canonical: production
  wire format produces correct grading + canonical-letter storage.
- test_submit_failsafe_locks_in_zero_score_on_garbage_text: a non-
  matching string is recorded at score=0 and a follow-up correct
  submission cannot overwrite it.

71/71 green.
2026-05-04 17:31:12 +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%