feat(geom9 ch3 wave2 + final): §12 «Герон» + Финал Главы 3

This commit is contained in:
Maxim Dolgolyov
2026-05-29 10:13:29 +03:00
parent 8dcd54d206
commit 1b79965fce
13 changed files with 1320 additions and 10 deletions
+86
View File
@@ -0,0 +1,86 @@
'use strict';
const router = require('express').Router();
const db = require('../db/db');
const { authMiddleware } = require('../middleware/auth');
router.use(authMiddleware);
/* ── Statements (prepared once) ────────────────────────────────── */
const SQL = {
listTracks: db.prepare(`
SELECT exam_key, title, subject_slug, grade, duration_min,
tasks_per_variant, variants_count, intro_html, sort_order
FROM exam_tracks
WHERE enabled = 1
ORDER BY sort_order, exam_key
`),
getTrack: db.prepare(`
SELECT exam_key, title, subject_slug, grade, duration_min,
tasks_per_variant, variants_count, scoring_json, intro_html
FROM exam_tracks
WHERE exam_key = ? AND enabled = 1
`),
countTasks: db.prepare(`
SELECT
COUNT(*) AS total,
COALESCE(SUM(CASE WHEN task_type = 'mc' THEN 1 ELSE 0 END), 0) AS mc,
COALESCE(SUM(CASE WHEN task_type = 'open' THEN 1 ELSE 0 END), 0) AS open,
COALESCE(SUM(CASE WHEN task_type = 'long' THEN 1 ELSE 0 END), 0) AS long
FROM exam_tasks
WHERE exam_key = ?
`),
/* For each (user, task) — was the most-recent attempt correct?
A task counts as «solved» iff at least one historical attempt is_correct = 1. */
userProgress: db.prepare(`
SELECT
COUNT(DISTINCT exam_task_id) AS tasks_attempted,
COUNT(DISTINCT CASE WHEN is_correct = 1 THEN exam_task_id END) AS tasks_solved,
COUNT(*) AS total_attempts,
COALESCE(SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END), 0) AS correct_attempts
FROM exam_attempts a
JOIN exam_tasks t ON t.id = a.exam_task_id
WHERE a.user_id = ? AND t.exam_key = ?
`),
};
/* ── GET /api/exam-prep/tracks ──
Public list of enabled exam tracks (for a future landing page). */
router.get('/tracks', (_req, res) => {
const tracks = SQL.listTracks.all();
res.json({ tracks });
});
/* ── GET /api/exam-prep/:examKey/info ──
Track metadata + global counts + this user's aggregate progress. */
router.get('/:examKey/info', (req, res) => {
const { examKey } = req.params;
const track = SQL.getTrack.get(examKey);
if (!track) return res.status(404).json({ error: 'Unknown exam track' });
const counts = SQL.countTasks.get(examKey);
const progress = SQL.userProgress.get(req.user.id, examKey);
// Parse scoring grid (JSON in DB). Tolerant of malformed values.
let scoring = null;
if (track.scoring_json) {
try { scoring = JSON.parse(track.scoring_json); } catch { scoring = null; }
}
res.json({
track: {
exam_key: track.exam_key,
title: track.title,
subject: track.subject_slug,
grade: track.grade,
duration_min: track.duration_min,
tasks_per_variant: track.tasks_per_variant,
variants_count: track.variants_count,
intro_html: track.intro_html,
scoring,
},
counts,
progress,
});
});
module.exports = router;