feat(exam): log time spent per scenario in exam report

Signed-off-by: Abhinav Sinha <[email protected]>
This commit is contained in:
Abhinav Sinha
2026-06-16 16:29:50 +05:30
parent 6766d772d1
commit 4acdb0df40
5 changed files with 56 additions and 0 deletions
+4
View File
@@ -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);
+17
View File
@@ -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 && (
<span className={styles.rowAttempts}>{s.attempts} attempt{s.attempts > 1 ? 's' : ''}</span>
)}
<span className={styles.rowTime}>
{(s.time_spent_seconds > 0) && (
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 3, verticalAlign: 'middle', opacity: 0.6 }}>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
)}
{formatRowTime(s.time_spent_seconds)}
</span>
<span className={styles.rowPts}>
{s.status === 'completed' ? s.weight : 0}/{s.weight} pts
</span>
@@ -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;
+17
View File
@@ -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 }) {
<span className={styles.rowDiff} style={{ color: DIFF_COLOR[s.difficulty] }}>
{s.difficulty}
</span>
<span className={styles.rowTime}>
{s.time_spent_seconds > 0 && (
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 3, verticalAlign: 'middle', opacity: 0.6 }}>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
)}
{formatRowTime(s.time_spent_seconds)}
</span>
<span className={styles.rowPts}>{s.status === 'completed' ? s.weight : 0}/{s.weight} pts</span>
</div>
))}
@@ -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 */