fix: a skipped question is not a wrong answer

Performance by category counted every row in an attempt, and an attempt holds a
row for each question including the ones never answered. A 360-question sitting
that was opened and abandoned therefore landed as 360 wrong answers, which is
why Emergency Medicine read 0% of 400 and Gastroenterology 1.1% of 277 — figures
that describe a sitting nobody worked through, not a learner who cannot do
emergency medicine.

Accuracy now counts only questions that were actually answered, and the note
under the heading says so. Coverage is a separate question from accuracy and
conflating them made both useless.

Also: the category performance block is gone from the dashboard, where it
duplicated the one on Analysis; and the nav says Sessions rather than Quizzes,
with History beside it — "quiz" describes the packaging, a learner sits a
session, and the two entries answer different questions: what can I sit, and
what have I sat.

208 backend, 249 frontend green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-11 03:21:13 +02:00
parent 0e6c18d886
commit 5add9f23dd
3 changed files with 10 additions and 4 deletions

View file

@ -181,6 +181,10 @@ def performance_by_category(db: Session = Depends(get_db), user: User = Depends(
Question, Question.id == AttemptAnswer.question_id).filter(
QuizAttempt.user_id == user.id, QuizAttempt.completed_at.isnot(None),
or_(QuizAttempt.expired == 0, QuizAttempt.expired.is_(None)), Quiz.course_id.is_(None),
# A skipped question is not a wrong answer. Counting it as one made a
# 360-question sitting that was never worked through read as 0% accuracy
# across every category it touched.
AttemptAnswer.user_answer.isnot(None), AttemptAnswer.user_answer != "",
).all()
extra: dict[int, set[int]] = defaultdict(set)
for qid, cid in db.query(QuestionCategoryLink.question_id, QuestionCategoryLink.category_id).all():
@ -206,7 +210,7 @@ def performance_by_category(db: Session = Depends(get_db), user: User = Depends(
return {
"total_answered": sum(row["answered"] for row in categories),
"categories": categories,
"basis": "Your completed, non-expired general test answers; a question counts in every category it belongs to.",
"basis": "Questions you actually answered in completed sessions; skipped ones are left out, and a question counts in every category it belongs to.",
}

View file

@ -107,7 +107,11 @@ export default function Navbar({ onSignIn, onRegister }) {
{ to: '/home', label: 'Home' },
{ to: '/', label: 'Dashboard' },
{ to: '/ai', label: 'AI Mode' },
{ to: '/quizzes', label: 'Quizzes' },
// "Quiz" describes the packaging; a learner sits a session. Both entries
// are here because they answer different questions: what can I sit, and
// what have I sat.
{ to: '/quizzes', label: 'Sessions' },
{ to: '/sessions', label: 'History' },
{ to: '/analysis', label: 'Analysis' },
{ to: '/question-bank', label: 'Question Bank' },
...(canManageQuestions ? [{ to: '/questions/manage', label: 'Manage Qs' },

View file

@ -4,7 +4,6 @@ import { Link } from 'react-router-dom'
import api from '../api/client'
import LineChart from '../components/LineChart'
import InProgressQuizzes from '../components/InProgressQuizzes'
import CategoryPerformance from '../components/CategoryPerformance'
import MyNote from '../components/MyNote'
import { useAuth } from '../context/AuthContext'
@ -86,7 +85,6 @@ export default function DashboardPage() {
<InProgressQuizzes />
<CategoryPerformance />
<MyNote variant="card" />