feat(exam): log time spent per scenario in exam report
Signed-off-by: Abhinav Sinha <[email protected]>
This commit is contained in:
@@ -344,12 +344,16 @@ app.post('/api/sessions/:id/submit', (req, res) => {
|
|||||||
const examProgressMap = {};
|
const examProgressMap = {};
|
||||||
for (const r of examProgressRows) examProgressMap[r.scenario_id] = r;
|
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 => ({
|
const snapshot = bundleScenarios.map(s => ({
|
||||||
id: s.id, title: s.title, weight: s.weight,
|
id: s.id, title: s.title, weight: s.weight,
|
||||||
category: s.category, type: s.type, difficulty: s.difficulty,
|
category: s.category, type: s.type, difficulty: s.difficulty,
|
||||||
status: examProgressMap[s.id]?.status || 'not_started',
|
status: examProgressMap[s.id]?.status || 'not_started',
|
||||||
completed_at: examProgressMap[s.id]?.completed_at || null,
|
completed_at: examProgressMap[s.id]?.completed_at || null,
|
||||||
attempts: examProgressMap[s.id]?.attempts || 0,
|
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 startedAt = new Date(session.started_at + 'Z');
|
||||||
const durationSecs = Math.round((Date.now() - startedAt.getTime()) / 1000);
|
const durationSecs = Math.round((Date.now() - startedAt.getTime()) / 1000);
|
||||||
|
|||||||
@@ -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 }) {
|
function ScoreRing({ pct, passed, size = 72 }) {
|
||||||
const r = (size / 2) - 7
|
const r = (size / 2) - 7
|
||||||
const circ = 2 * Math.PI * r
|
const circ = 2 * Math.PI * r
|
||||||
@@ -169,6 +177,15 @@ function AttemptDetail({ attempt }) {
|
|||||||
{s.attempts > 0 && (
|
{s.attempts > 0 && (
|
||||||
<span className={styles.rowAttempts}>{s.attempts} attempt{s.attempts > 1 ? 's' : ''}</span>
|
<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}>
|
<span className={styles.rowPts}>
|
||||||
{s.status === 'completed' ? s.weight : 0}/{s.weight} pts
|
{s.status === 'completed' ? s.weight : 0}/{s.weight} pts
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -430,6 +430,15 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
flex-shrink: 0;
|
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 {
|
.rowPts {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ function formatDuration(secs) {
|
|||||||
return `${s}s`
|
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)' }
|
const DIFF_COLOR = { Easy: 'var(--green)', Medium: 'var(--amber)', Hard: 'var(--red)' }
|
||||||
|
|
||||||
export default function ExamReport({ report, bundle, onClose, onRetry }) {
|
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] }}>
|
<span className={styles.rowDiff} style={{ color: DIFF_COLOR[s.difficulty] }}>
|
||||||
{s.difficulty}
|
{s.difficulty}
|
||||||
</span>
|
</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>
|
<span className={styles.rowPts}>{s.status === 'completed' ? s.weight : 0}/{s.weight} pts</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -101,6 +101,15 @@
|
|||||||
.rowIcon { font-size: 14px; flex-shrink: 0; }
|
.rowIcon { font-size: 14px; flex-shrink: 0; }
|
||||||
.rowTitle { flex: 1; }
|
.rowTitle { flex: 1; }
|
||||||
.rowDiff { font-size: 11px; font-weight: 600; flex-shrink: 0; }
|
.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; }
|
.rowPts { font-family: var(--mono); font-size: 12px; color: var(--text-3); flex-shrink: 0; min-width: 60px; text-align: right; }
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|||||||
Reference in New Issue
Block a user