From 4acdb0df4068b334a8ef4b41984df7cc8562da1f Mon Sep 17 00:00:00 2001 From: Abhinav Sinha Date: Tue, 16 Jun 2026 16:29:50 +0530 Subject: [PATCH] feat(exam): log time spent per scenario in exam report Signed-off-by: Abhinav Sinha --- backend/server.js | 4 ++++ frontend/src/components/ExamHistory.jsx | 17 +++++++++++++++++ frontend/src/components/ExamHistory.module.css | 9 +++++++++ frontend/src/components/ExamReport.jsx | 17 +++++++++++++++++ frontend/src/components/ExamReport.module.css | 9 +++++++++ 5 files changed, 56 insertions(+) diff --git a/backend/server.js b/backend/server.js index 0675a0f..14d4100 100644 --- a/backend/server.js +++ b/backend/server.js @@ -344,12 +344,16 @@ app.post('/api/sessions/:id/submit', (req, res) => { const examProgressMap = {}; for (const r of examProgressRows) examProgressMap[r.scenario_id] = r; + // Also pull time_spent_seconds from global progress (tracked per scenario during the exam) + const globalProgress = loadProgress(); + const snapshot = bundleScenarios.map(s => ({ id: s.id, title: s.title, weight: s.weight, category: s.category, type: s.type, difficulty: s.difficulty, status: examProgressMap[s.id]?.status || 'not_started', completed_at: examProgressMap[s.id]?.completed_at || null, attempts: examProgressMap[s.id]?.attempts || 0, + time_spent_seconds: globalProgress[s.id]?.time_spent_seconds || 0, })); const startedAt = new Date(session.started_at + 'Z'); const durationSecs = Math.round((Date.now() - startedAt.getTime()) / 1000); diff --git a/frontend/src/components/ExamHistory.jsx b/frontend/src/components/ExamHistory.jsx index f192dfe..b0608a7 100644 --- a/frontend/src/components/ExamHistory.jsx +++ b/frontend/src/components/ExamHistory.jsx @@ -22,6 +22,14 @@ function formatDate(iso) { }) } +function formatRowTime(secs) { + if (!secs || secs === 0) return '—' + const m = Math.floor(secs / 60) + const s = secs % 60 + if (m > 0) return `${m}m ${s > 0 ? s + 's' : ''}`.trim() + return `${s}s` +} + function ScoreRing({ pct, passed, size = 72 }) { const r = (size / 2) - 7 const circ = 2 * Math.PI * r @@ -169,6 +177,15 @@ function AttemptDetail({ attempt }) { {s.attempts > 0 && ( {s.attempts} attempt{s.attempts > 1 ? 's' : ''} )} + + {(s.time_spent_seconds > 0) && ( + + + + + )} + {formatRowTime(s.time_spent_seconds)} + {s.status === 'completed' ? s.weight : 0}/{s.weight} pts diff --git a/frontend/src/components/ExamHistory.module.css b/frontend/src/components/ExamHistory.module.css index 8bc4af6..6bac197 100644 --- a/frontend/src/components/ExamHistory.module.css +++ b/frontend/src/components/ExamHistory.module.css @@ -430,6 +430,15 @@ border-radius: 3px; flex-shrink: 0; } +.rowTime { + font-family: var(--mono); + font-size: 11px; + color: var(--text-3); + flex-shrink: 0; + min-width: 52px; + display: flex; + align-items: center; +} .rowPts { font-family: var(--mono); font-size: 11px; diff --git a/frontend/src/components/ExamReport.jsx b/frontend/src/components/ExamReport.jsx index 88980c2..2ffb30b 100644 --- a/frontend/src/components/ExamReport.jsx +++ b/frontend/src/components/ExamReport.jsx @@ -10,6 +10,14 @@ function formatDuration(secs) { return `${s}s` } +function formatRowTime(secs) { + if (!secs || secs === 0) return '—' + const m = Math.floor(secs / 60) + const s = secs % 60 + if (m > 0) return `${m}m ${s > 0 ? s + 's' : ''}`.trim() + return `${s}s` +} + const DIFF_COLOR = { Easy: 'var(--green)', Medium: 'var(--amber)', Hard: 'var(--red)' } export default function ExamReport({ report, bundle, onClose, onRetry }) { @@ -86,6 +94,15 @@ export default function ExamReport({ report, bundle, onClose, onRetry }) { {s.difficulty} + + {s.time_spent_seconds > 0 && ( + + + + + )} + {formatRowTime(s.time_spent_seconds)} + {s.status === 'completed' ? s.weight : 0}/{s.weight} pts ))} diff --git a/frontend/src/components/ExamReport.module.css b/frontend/src/components/ExamReport.module.css index 6826b9b..884c165 100644 --- a/frontend/src/components/ExamReport.module.css +++ b/frontend/src/components/ExamReport.module.css @@ -101,6 +101,15 @@ .rowIcon { font-size: 14px; flex-shrink: 0; } .rowTitle { flex: 1; } .rowDiff { font-size: 11px; font-weight: 600; flex-shrink: 0; } +.rowTime { + font-family: var(--mono); + font-size: 11px; + color: var(--text-3); + flex-shrink: 0; + min-width: 52px; + display: flex; + align-items: center; +} .rowPts { font-family: var(--mono); font-size: 12px; color: var(--text-3); flex-shrink: 0; min-width: 60px; text-align: right; } /* Actions */