Backend simplification:
- The server now loads ONE pool JSON from $QUIZ_POOL_PATH at startup and
upserts a single canonical session. The session id comes from the pool
JSON's optional "session_id" field, falling back to $QUIZ_SESSION_ID.
- The multi-quiz / multi-session CRUD API is gone:
DELETED GET/POST /admin/api/quizzes
DELETED POST /admin/api/quizzes/upload
DELETED GET/POST /admin/api/sessions
DELETED GET /admin/login (HTML stub)
DELETED GET /admin/api/sessions/{sid}/csv (replaced by /admin/api/csv)
Replaced with a single-session control surface:
GET /admin/ — serves admin.html unconditionally
GET /admin/api/state — admin-gated; pool meta + state + QR + join URL
POST /admin/api/reset — admin-gated; wipe submissions + back to lobby
POST /admin/logout — clear admin cookie
GET /admin/api/csv — single-session results
WS /ws/instructor/{sid} — kept; new commands "next" + "reset"
- Instructor "Next" button is now a single state-driving command
(RoomManager.advance_to_next): from lobby it opens Q0; from question_open
it closes the current Q and opens the next; from question_closed it
opens the next; if past the last question it ends the session.
- New RoomManager.reset wipes submissions, participants, and per-question
state, then broadcasts a clean lobby.
- Student GET / now redirects to /?sid=<canonical> when no sid is given,
so the QR / share URL is fully deterministic.
Frontend rewrite (functional baseline; visual polish to follow):
- /admin/ is now a single SPA: GET /admin/api/state decides login form
vs dashboard. No separate /admin/login URL bar.
- Admin dashboard is state-driven with one primary action per state.
QR code, join URL, and live participant list are always visible on the
left so the operator can leave the page on a projector.
- Student answer buttons are big and tappable; reveal screen highlights
correct/wrong choice + shows score, total, and rank.
- Static admin/student SPAs share a CSS palette with light/dark support.
Tests rewritten around the single canonical session id.
The auto-bootstrapped session lets each test fixture skip the old
quiz/session creation dance. 39/39 tests pass.
Cleanup:
- Deleted CODEX_PROMPT.md, IMPLEMENTATION_REPORT.md, NOTES.md, SPEC.md,
static/observer.html (obsolete codex-build artifacts and the unused
observer view).
- .gitignore now blocks /pool.json (the runtime file the operator drops
on the server) and the leftover .codex_done / codex_run.log / etc.
- bootstrap.sh seeds /opt/quiz/pool.json from examples/pool_example.json
on first deploy so a fresh box reaches a usable state without manual
intervention; .env now includes QUIZ_POOL_PATH.
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).