overhaul: single-session deployment + redesigned frontend
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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Student routes."""
|
||||
"""Student routes (single-session deployment)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, WebSocket
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
|
||||
from app import auth
|
||||
from app.config import Settings
|
||||
@@ -17,13 +17,27 @@ from app.room import RoomManager
|
||||
def router(settings: Settings, rooms: RoomManager) -> APIRouter:
|
||||
api = APIRouter()
|
||||
|
||||
def resolve_sid(sid: str | None) -> str:
|
||||
return sid if sid else (rooms.canonical_sid or settings.default_session_id)
|
||||
|
||||
@api.get("/")
|
||||
async def student_entry(sid: str | None = None):
|
||||
if not sid or not await rooms.session_exists(sid):
|
||||
target_sid = resolve_sid(sid)
|
||||
if not await rooms.session_exists(target_sid):
|
||||
return HTMLResponse(
|
||||
"<!doctype html><title>Quiz</title><main><h1>Ask your instructor for the link</h1>"
|
||||
"<p>This quiz link is missing or no longer valid.</p></main>"
|
||||
"<!doctype html><meta charset='utf-8'>"
|
||||
"<link rel='stylesheet' href='/static/style.css'>"
|
||||
"<title>Quiz unavailable</title>"
|
||||
"<main class='centered-shell'><div class='card narrow'>"
|
||||
"<h1>Ask your instructor for the link</h1>"
|
||||
"<p class='muted'>This quiz link is missing or no longer valid.</p>"
|
||||
"</div></main>",
|
||||
status_code=404,
|
||||
)
|
||||
if not sid:
|
||||
# Canonicalise the URL so QR codes, share links, and bookmarks
|
||||
# all converge on the same sid-bearing form.
|
||||
return RedirectResponse(url=f"/?sid={target_sid}", status_code=302)
|
||||
return FileResponse(Path("static/student.html"))
|
||||
|
||||
@api.get("/api/session/{sid}")
|
||||
@@ -32,6 +46,7 @@ def router(settings: Settings, rooms: RoomManager) -> APIRouter:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
session = await rooms.get_session(sid)
|
||||
return {
|
||||
"sid": sid,
|
||||
"title": session["title"],
|
||||
"state": session["state"],
|
||||
"current_question_idx": session["current_question_idx"],
|
||||
|
||||
Reference in New Issue
Block a user