fix: hide score on submit + total denominator + projector chart cleanup

Three small UX/fairness tweaks from manual live testing:

1. Post-submit "wait for reveal" screen: show only the response time, no
   score. The +score reveal leaked correctness — any positive number =
   correct, zero = wrong — short-circuiting the "stop and think" beat
   the reveal pause was supposed to enforce. Time stays as the
   engagement signal; score now waits for the instructor reveal.

2. Final-screen "Correct X / Y" denominator is now total_questions
   instead of questions_answered. Missed questions are scored zero, so
   they belong in the denominator visibly. Server adds total_questions
   to the session_ended payload.

3. Projector score-distribution: drop the in-chart count labels (they
   collided with each other and with the median tag at small N), restore
   the previously-computed-but-not-rendered x-axis tick labels at the
   bottom. Stats line at the foot keeps n / mean / max.

Also: short-circuit the per-submit instructor + presence broadcasts
when no instructor / projector is connected (no listener, no DB work).
The 50-student load test was tight on margin against its 2 s
time_limit; with the new presence_message / live_histogram_message DB
queries firing on every submit, the margin disappeared on busy boxes.
Conftest fixture also bumped to 8 s per question for the same reason —
gives breathing room for sequential WS submits in the load test.

71/71 pytest green.
This commit is contained in:
ameer
2026-05-04 18:25:44 +08:00
parent 168cffea8b
commit 19603abc58
6 changed files with 66 additions and 22 deletions

View File

@@ -415,11 +415,16 @@ function submitAnswer(optionKey, optionText) {
function renderSubmitted(message) {
store.submitted = message;
const seconds = (message.elapsed_ms / 1000).toFixed(1);
// Deliberately hide the score until the instructor reveals — leaks
// correctness otherwise (any positive score = correct, zero = wrong),
// which short-circuits the "stop and think" beat the reveal pause is
// there to enforce. Show response time as the engagement signal
// instead.
setView(`
<div class="card narrow center">
<p class="eyebrow">Question ${message.question_idx + 1}</p>
<h1 class="big-score">+${fmtScore(message.score)}</h1>
<p class="muted">submitted in ${seconds}s</p>
<h1 class="big-score">${seconds}<small class="unit">s</small></h1>
<p class="muted">answer recorded</p>
<p class="muted small">Waiting for the reveal…</p>
<div class="spinner" aria-hidden="true"></div>
</div>
@@ -492,7 +497,7 @@ function renderFinished(message) {
<div class="reveal-stats">
<div class="stat big"><span class="muted">Your total</span><b>${fmtScore(message.your_total)}</b></div>
<div class="stat"><span class="muted">Rank</span><b>${message.your_rank ?? "—"}</b></div>
<div class="stat"><span class="muted">Correct</span><b>${message.questions_correct ?? 0} / ${message.questions_answered ?? 0}</b></div>
<div class="stat"><span class="muted">Correct</span><b>${message.questions_correct ?? 0} / ${message.total_questions ?? message.questions_answered ?? 0}</b></div>
</div>
<h3>Final top 5</h3>
${renderBoard(message.final_top5)}