diff --git a/backend/scripts/seed_ctmath_rt2324_e1v1.js b/backend/scripts/seed_ctmath_rt2324_e1v1.js
new file mode 100644
index 0000000..5481bcb
--- /dev/null
+++ b/backend/scripts/seed_ctmath_rt2324_e1v1.js
@@ -0,0 +1,364 @@
+'use strict';
+/* ───────────────────────────────────────────────────────────────────────────
+ seed_ctmath_rt2324_e1v1.js
+ Чистый вариант-пробник для трека exam-prep `ctmath`.
+
+ Источник: РТ–2023/2024, Этап I, Вариант 1 (РИКЗ, «Тематическое
+ консультирование по математике»). 30 заданий: А1–А10 + В1–В20.
+ Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR):
+ F:\!Рабочие\ЦТ\Математика\Математика\РТ\2023-2024\МАТ РТ-1 23_24 В1.pdf
+
+ variant=104 — следующий «чистый» вариант после РТ-2024/25 (101/102/103).
+ Геометрия закодирована текстом (стандартная разметка фигур / углы словами) —
+ отдельных чертежей не требуется (как у большинства существующих задач).
+
+ Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
+ Запуск:
+ node backend/scripts/seed_ctmath_rt2324_e1v1.js # DRY-RUN (по умолчанию)
+ node backend/scripts/seed_ctmath_rt2324_e1v1.js --apply # запись в БД
+
+ ⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code
+ блокирует продакшн-записи). Без --apply ничего не пишется.
+ ─────────────────────────────────────────────────────────────────────────── */
+
+const { DatabaseSync } = require('node:sqlite');
+const path = require('path');
+
+const APPLY = process.argv.includes('--apply');
+const EXAM = 'ctmath';
+const VARIANT = 104;
+const PROV = 'РТ–2023/2024, Этап I, Вариант 1';
+const R = String.raw;
+
+/* opts: метки кириллица а–д (как в существующих строках ctmath; checkAnswerServer
+ имеет ветку /^[а-д]$/). РТ-варианты 1..5 → а..д. */
+const L = ['а', 'б', 'в', 'г', 'д'];
+const mc = (...html) => html.map((h, i) => [L[i], h]);
+
+/* ── 30 заданий ─────────────────────────────────────────────────────────── */
+const TASKS = [
+ // ── Часть A: А1–А10 ──────────────────────────────────────────────────────
+ { idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
+ text: R`Наименьшее целое число, принадлежащее интервалу $(-13{,}8;-7)$, равно:`,
+ opts: mc('$-14$', '$-6$', '$-7$', '$-13$', '$-12$'),
+ answer: 'г',
+ sol: R`Интервалу $(-13{,}8;-7)$ принадлежат целые числа $-13,\,-12,\,-11,\,-10,\,-9,\,-8$. Наименьшее из них — число $-13$.`,
+ ref: 'Арефьева «Алгебра, 8 кл.», гл. 1, § 5' },
+
+ { idx: 2, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1,
+ text: R`Укажите номер выражения, которое является произведением числа $m$ и суммы чисел $1{,}7$ и $3{,}5$.`,
+ opts: mc('$1{,}7\cdot(3{,}5+m)$', '$1{,}7+3{,}5\cdot m$', '$1{,}7\cdot m+3{,}5$', '$3{,}5\cdot(1{,}7+m)$', '$m\cdot(1{,}7+3{,}5)$'),
+ answer: 'д',
+ sol: R`Произведение числа $m$ и суммы чисел $1{,}7$ и $3{,}5$ записывается как $m\cdot(1{,}7+3{,}5)$.`,
+ ref: 'Арефьева «Алгебра, 7 кл.», гл. 2, § 4' },
+
+ { idx: 3, type: 'mc', topic: 'stereometry', subtopic: 'ster-basics', diff: 2,
+ text: R`В параллелепипеде $ABCDA_1B_1C_1D_1$ среди отрезков $BD$, $BD_1$, $AB_1$, $A_1B_1$, $B_1B$ укажите тот, который является диагональю параллелепипеда.`,
+ opts: mc('$BD$', '$BD_1$', '$AB_1$', '$A_1B_1$', '$B_1B$'),
+ answer: 'б',
+ sol: R`Диагональю параллелепипеда называют отрезок, соединяющий две вершины, не принадлежащие одной грани. Среди перечисленных только $BD_1$ соединяет противоположные вершины параллелепипеда, поэтому $BD_1$ — диагональ.`,
+ ref: 'Латотин «Геометрия, 10 кл.», разд. 1, § 1' },
+
+ { idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 1,
+ text: R`Определите, при каком из значений $x$, равных $4;\ 5;\ 1;\ 0{,}1;\ 8$, верно неравенство $\dfrac{320}{x}<50$.`,
+ opts: mc('$4$', '$5$', '$1$', '$0{,}1$', '$8$'),
+ answer: 'д',
+ sol: R`При $x>0$ неравенство $\dfrac{320}{x}<50$ равносильно $x>6{,}4$. Из данных чисел этому условию удовлетворяет только $x=8$.`,
+ ref: 'Арефьева «Алгебра, 7 кл.», гл. 3, § 18' },
+
+ { idx: 5, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
+ text: R`Укажите номер функции, график которой параллелен оси абсцисс.`,
+ opts: mc('$y=-4$', '$y=4-2x$', '$y=\dfrac{2}{x}$', '$y=4^{x}$', '$y=4x$'),
+ answer: 'а',
+ sol: R`График параллелен оси абсцисс у постоянной функции $y=b$ (угловой коэффициент $k=0$). Из предложенных это $y=-4$.`,
+ ref: 'Арефьева «Алгебра, 7 кл.», гл. 3, § 20' },
+
+ { idx: 6, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 2,
+ text: R`Функция задана формулой $f(x)=|x|-6$. Укажите номера верных утверждений.
1) областью определения функции является множество всех действительных чисел;
2) функция возрастает на промежутке $(-\infty;0]$;
3) функция является чётной;
4) $f(-5)=-11$;
5) $f(3)<0$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`,
+ answer: '135', ansShow: '1, 3, 5',
+ sol: R`$1)$ верно: $|x|-6$ определено при всех $x$. $\ 2)$ неверно: на $(-\infty;0]$ функция убывает. $\ 3)$ верно: $f(-x)=|-x|-6=|x|-6=f(x)$. $\ 4)$ неверно: $f(-5)=5-6=-1$. $\ 5)$ верно: $f(3)=3-6=-3<0$.`,
+ ref: 'Арефьева «Алгебра, 8 кл.», гл. 4, § 19' },
+
+ { idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
+ text: R`Фермер привёз на осеннюю ярмарку некоторое количество картофеля и продал из этого количества $102$ кг. Сколько всего килограммов картофеля привёз фермер, если после продажи осталось $\dfrac{5}{11}$ всего привезённого картофеля?`,
+ opts: mc('$224$ кг', '$204$ кг', '$192$ кг', '$187$ кг', '$169$ кг'),
+ answer: 'г',
+ sol: R`Проданная часть составляет $1-\dfrac{5}{11}=\dfrac{6}{11}$ всего картофеля и равна $102$ кг. Тогда всего привезено $102:\dfrac{6}{11}=\dfrac{102\cdot11}{6}=187$ кг.`,
+ ref: 'Герасимов «Математика, 6 кл.», гл. 3, § 10' },
+
+ { idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 1,
+ text: R`Значение выражения $\sqrt[3]{0{,}9}\cdot\sqrt[3]{30}$ равно:`,
+ opts: mc('$3$', '$\sqrt[3]{12}$', '$\sqrt3$', '$\sqrt[6]{12}$', '$6$'),
+ answer: 'а',
+ sol: R`По свойству корня $n$-й степени $\sqrt[3]{0{,}9}\cdot\sqrt[3]{30}=\sqrt[3]{0{,}9\cdot30}=\sqrt[3]{27}=3$.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 2, § 14' },
+
+ { idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 2,
+ text: R`Треугольник $KMN$ — сечение треугольной пирамиды $SABC$ плоскостью, проходящей через точку $M$ — середину ребра $BC$ — параллельно плоскости $SAC$. Найдите периметр треугольника $KMN$, если каждое ребро пирамиды $SABC$ имеет длину $2\sqrt2$.`,
+ opts: mc('$\dfrac{2\sqrt2}{3}$', '$\dfrac{3\sqrt2}{2}$', '$3\sqrt2$', '$6\sqrt2$', '$4\sqrt2$'),
+ answer: 'в',
+ sol: R`Секущая плоскость параллельна $SAC$ и проходит через середину $M$ ребра $BC$, поэтому она пересекает рёбра $AB$ и $SB$ в их серединах $N$ и $K$. Отрезки $MN$, $MK$, $NK$ — средние линии граней, каждый равен $\dfrac12\cdot2\sqrt2=\sqrt2$. Значит, периметр $KMN$ равен $3\sqrt2$.`,
+ ref: 'Латотин «Геометрия, 10 кл.», разд. 1, § 3' },
+
+ { idx: 10, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
+ text: R`Укажите номера выражений, которые не имеют смысла.
1) $\arccos\dfrac{\sqrt3}{2}$;
2) $\arcsin\sqrt2$;
3) $\operatorname{ctg}\left(-\dfrac{3\pi}{2}\right)$;
4) $\operatorname{tg}\left(-\dfrac{3\pi}{2}\right)$;
5) $\operatorname{arctg}\dfrac{\sqrt3}{3}$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`,
+ answer: '24', ansShow: '2, 4',
+ sol: R`$1)$ имеет смысл: $\arccos\dfrac{\sqrt3}{2}=\dfrac{\pi}{6}$. $\ 2)$ не имеет смысла: $\sqrt2\notin[-1;1]$. $\ 3)$ имеет смысл: $\operatorname{ctg}\left(-\dfrac{3\pi}{2}\right)=0$. $\ 4)$ не имеет смысла: $\operatorname{tg}\left(-\dfrac{3\pi}{2}\right)$ не существует, так как $\cos\left(-\dfrac{3\pi}{2}\right)=0$. $\ 5)$ имеет смысл: $\operatorname{arctg}\dfrac{\sqrt3}{3}=\dfrac{\pi}{6}$.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 1, § 3; § 7' },
+
+ // ── Часть B: В1–В20 ──────────────────────────────────────────────────────
+ { idx: 11, type: 'long', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
+ text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.
Начало:
А) Наибольший простой делитель числа $14$ равен …
Б) Наименьшее общее кратное чисел $5$ и $55$ равно …
В) Наибольший общий делитель чисел $16$ и $55$ равен …
Окончание:
1) $1$; 2) $110$; 3) $55$; 4) $5$; 5) $7$; 6) $2$.
Ответ запишите сочетанием букв и цифр, например: А1Б1В4.`,
+ answer: 'А5Б3В1', ansShow: 'А5Б3В1',
+ sol: R`А) Простые делители числа $14$ — это $2$ и $7$; наибольший равен $7$ — окончание 5. Б) Наименьшее общее кратное чисел $5$ и $55$ равно $55$ — окончание 3. В) Наибольший общий делитель чисел $16$ и $55$ равен $1$ — окончание 1.`,
+ ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 1, § 12' },
+
+ { idx: 12, type: 'open', topic: 'stereometry', subtopic: 'ster-basics', diff: 3,
+ text: R`$ABCDA_1B_1C_1D_1$ — куб. Отрезки $B_1D_1$ и $AD_1$ являются диагоналями граней $A_1B_1C_1D_1$ и $AA_1D_1D$ соответственно. Выберите верные утверждения.
1) прямая $C_1D_1$ перпендикулярна прямой $AD_1$;
2) прямая $B_1D_1$ параллельна прямой $BC$;
3) прямая $AD_1$ параллельна плоскости $BB_1C_1$;
4) прямая $B_1D_1$ перпендикулярна прямой $AD_1$;
5) прямая $AA_1$ параллельна прямой $B_1D_1$;
6) прямая $CC_1$ параллельна плоскости $BAA_1$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`,
+ answer: '136', ansShow: '1, 3, 6',
+ sol: R`$1)$ верно: $C_1D_1$ перпендикулярна плоскости $AA_1D_1D$, значит $C_1D_1\perp AD_1$. $\ 2)$ неверно: $B_1D_1$ и $BC$ скрещиваются. $\ 3)$ верно: $AD_1\subset AA_1D_1D$, а эта грань параллельна грани $BB_1C_1C$. $\ 4)$ неверно: угол между $B_1D_1$ и $AD_1$ равен $60^\circ$. $\ 5)$ неверно: $AA_1\perp B_1D_1$. $\ 6)$ верно: $CC_1\subset CC_1D_1D$, а эта грань параллельна грани $BAA_1B_1$.`,
+ ref: 'Латотин «Геометрия, 10 кл.», разд. 1–2' },
+
+ { idx: 13, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 1,
+ text: R`В треугольнике $ABC$ известно, что $\angle ABC=40^\circ$, $\angle BAC=3x$, $\angle ACB=x$. Найдите градусную меру угла $ACB$.`,
+ answer: '35',
+ sol: R`По теореме о сумме градусных мер углов треугольника $3x+x+40^\circ=180^\circ$, откуда $4x=140^\circ$, $x=35^\circ$. Значит, $\angle ACB=35^\circ$.`,
+ ref: 'Казаков «Геометрия, 7 кл.», гл. 4, § 19' },
+
+ { idx: 14, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
+ text: R`Арифметическая прогрессия $(a_n)$ задана формулой $n$-го члена $a_n=6-2n$. Найдите номер члена этой прогрессии, равного $-70$.`,
+ answer: '38',
+ sol: R`По условию $a_n=-70$, тогда $6-2n=-70$, $-2n=-76$, $n=38$.`,
+ ref: 'Арефьева «Алгебра, 9 кл.», гл. 4, § 15' },
+
+ { idx: 15, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
+ text: R`Найдите произведение наименьшего натурального двузначного простого числа и натурального числа, при делении которого на $5$ получается в неполном частном $13$ и в остатке $1$.`,
+ answer: '726',
+ sol: R`Наименьшее двузначное простое число — $11$. Натуральное число с неполным частным $13$ и остатком $1$ при делении на $5$ равно $13\cdot5+1=66$. Произведение: $11\cdot66=726$.`,
+ ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 1, § 11' },
+
+ { idx: 16, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
+ text: R`За керамическую плитку и её укладку заплатили $524$ рубля. Сколько стоит (в рублях) керамическая плитка, если стоимость её укладки составляет $31\%$ стоимости плитки?`,
+ answer: '400',
+ sol: R`Пусть стоимость плитки равна $x$ рублей, тогда стоимость укладки $0{,}31x$. Уравнение $x+0{,}31x=524$, $1{,}31x=524$, $x=400$.`,
+ ref: 'Герасимов «Математика, 6 кл.», гл. 2, § 1' },
+
+ { idx: 17, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
+ text: R`Найдите значение выражения $\dfrac{a^{5}-a}{a^{5}-a^{9}}$ при $a=\dfrac{1}{\sqrt[4]{18}}$.`,
+ answer: '-18',
+ sol: R`$\dfrac{a^{5}-a}{a^{5}-a^{9}}=\dfrac{a(a^{4}-1)}{a^{5}(1-a^{4})}=-\dfrac{1}{a^{4}}$. При $a=\dfrac{1}{\sqrt[4]{18}}$ имеем $a^{4}=\dfrac{1}{18}$, поэтому значение равно $-18$.`,
+ ref: 'Арефьева «Алгебра, 11 кл.», гл. 1, § 1' },
+
+ { idx: 18, type: 'open', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
+ text: R`Дана функция $y=x^{2}$. График функции $y=g(x)$ получен из графика функции $y=x^{2}$ сдвигом вдоль оси абсцисс на $1$ единицу влево и вдоль оси ординат на $3$ единицы вниз. Найдите значение $g(-6)$.`,
+ answer: '22',
+ sol: R`Указанный сдвиг даёт $g(x)=(x+1)^{2}-3$. Тогда $g(-6)=(-6+1)^{2}-3=25-3=22$.`,
+ ref: 'Арефьева «Алгебра, 9 кл.», гл. 2, § 9' },
+
+ { idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
+ text: R`Найдите сумму наименьшего и наибольшего целых решений двойного неравенства $-130<\dfrac{4-3x}{0{,}5}<25$.`,
+ answer: '20',
+ sol: R`Умножив на $0{,}5$: $-65<4-3x<12{,}5$. Вычтя $4$: $-69<-3x<8{,}5$. Разделив на $-3$ (знаки меняются): $-2\dfrac560$ приводит к $v^{2}+5v-168\ge0$, решение $v\ge\dfrac{-5+\sqrt{697}}{2}\approx10{,}7$. Наименьшее целое значение $v=11$.`,
+ ref: 'Арефьева «Алгебра, 9 кл.», гл. 3, § 13' },
+
+ { idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
+ text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, все рёбра которой равны. Точка $N$ лежит на диагонали $A_1B$ грани $AA_1B_1B$ так, что $A_1N:NB=1:5$. Точки $M$ и $K$ лежат на рёбрах $CC_1$ и $CB$ соответственно так, что $CM:CC_1=1:4$, $CK:KB=1:3$. Найдите значение выражения $18\sqrt7\cdot\operatorname{tg}\varphi$, где $\varphi$ — угол между прямыми $C_1N$ и $KM$.`,
+ answer: '70',
+ sol: R`Пусть длина ребра призмы равна $a$. Прямая $BC_1\parallel KM$, поэтому угол между $C_1N$ и $KM$ равен углу $NC_1B$. Тогда $BC_1=a\sqrt2$, $A_1N=\dfrac{a\sqrt2}{6}$, $NB=\dfrac{5a\sqrt2}{6}$. Из треугольника $A_1BC_1$ по теореме косинусов $\cos\angle A_1BC_1=\dfrac34$; далее $C_1N=\dfrac{2a\sqrt2}{3}$ и $\cos\angle NC_1B=\dfrac{9}{16}$, $\sin\angle NC_1B=\dfrac{5\sqrt7}{16}$, $\operatorname{tg}\varphi=\dfrac{5\sqrt7}{9}$. Тогда $18\sqrt7\cdot\dfrac{5\sqrt7}{9}=2\cdot5\cdot7=70$.`,
+ ref: 'Латотин «Геометрия, 10 кл.», разд. 2, § 4' },
+];
+
+/* ── Сборка solution_html ────────────────────────────────────────────────── */
+function ansShowOf(t) {
+ if (t.ansShow != null) return t.ansShow;
+ if (t.type === 'mc') return `${t.answer})`;
+ return `$${t.answer}$`;
+}
+function buildSolution(t) {
+ const ans = ansShowOf(t);
+ let html = `${t.sol}Ответ: ${ans}
`;
+ if (t.ref) html += `Учебник: ${t.ref}
`;
+ return html;
+}
+
+/* ── Самопроверка (повтор логики checkAnswerServer из exam-prep.js) ────────── */
+const EPS = 1e-6;
+function srvToNumber(s) {
+ if (s == null) return NaN;
+ let t = String(s).trim().replace(/\$/g, '').replace(/\s+/g, '').replace(',', '.');
+ const f = t.match(/^(-?\d+(?:\.\d+)?)\s*\/\s*(-?\d+(?:\.\d+)?)$/);
+ if (f) { const n = Number(f[1]), d = Number(f[2]); return d === 0 ? NaN : n / d; }
+ const n = Number(t); return Number.isFinite(n) ? n : NaN;
+}
+function checkAnswerServer(userInput, canonical) {
+ if (userInput == null || canonical == null) return false;
+ const c = String(canonical).trim();
+ if (/^[а-д]$/.test(c)) return String(userInput).trim().toLowerCase() === c.toLowerCase();
+ if (/^[^;]+;[^;]+$/.test(c)) return false;
+ const cn = srvToNumber(c), un = srvToNumber(userInput);
+ if (Number.isNaN(cn) || Number.isNaN(un)) return false;
+ return Math.abs(cn - un) < EPS;
+}
+
+/* ── Валидация набора ──────────────────────────────────────────────────────── */
+const problems = [];
+if (TASKS.length !== 30) problems.push(`Ожидалось 30 заданий, получено ${TASKS.length}`);
+const seen = new Set();
+for (const t of TASKS) {
+ if (seen.has(t.idx)) problems.push(`Дубль task_idx=${t.idx}`); seen.add(t.idx);
+ if (t.idx < 1 || t.idx > 30) problems.push(`task_idx вне 1..30: ${t.idx}`);
+ if (!['mc', 'open', 'long'].includes(t.type)) problems.push(`#${t.idx}: тип ${t.type}`);
+ if (t.type === 'mc') {
+ if (!Array.isArray(t.opts) || t.opts.length !== 5) problems.push(`#${t.idx}: mc должен иметь 5 вариантов`);
+ if (!t.opts.some(o => o[0] === t.answer)) problems.push(`#${t.idx}: answer "${t.answer}" не среди меток`);
+ }
+ if (!t.text || !t.sol) problems.push(`#${t.idx}: пустой text/sol`);
+ if (t.type !== 'long' && !checkAnswerServer(t.answer, t.answer))
+ problems.push(`#${t.idx}: answer "${t.answer}" не проходит self-check (Unicode-минус? пробел?)`);
+ if (/−/.test(String(t.answer))) problems.push(`#${t.idx}: Unicode-минус в answer`);
+}
+
+/* ── Экспорт для тестов/тиража (без запуска main при require) ──────────────── */
+module.exports = { TASKS, buildSolution, ansShowOf, checkAnswerServer, EXAM, VARIANT, PROV };
+if (require.main !== module) return;
+
+/* ── Открытие БД ───────────────────────────────────────────────────────────── */
+const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
+const db = new DatabaseSync(DB);
+
+const track = db.prepare(`SELECT exam_key, variants_count FROM exam_tracks WHERE exam_key=?`).get(EXAM);
+if (!track) { console.error(`✗ Трек '${EXAM}' не найден в exam_tracks. Прерывание.`); process.exit(1); }
+
+/* ── DRY-RUN сводка ────────────────────────────────────────────────────────── */
+console.log(`\n=== seed_ctmath_rt2324_e1v1 (${PROV}) variant=${VARIANT} ===`);
+console.log(`Режим: ${APPLY ? 'APPLY (запись)' : 'DRY-RUN (только проверка)'}\n`);
+
+const byType = TASKS.reduce((a, t) => (a[t.type] = (a[t.type] || 0) + 1, a), {});
+console.log('Типы:', JSON.stringify(byType), '\n');
+
+console.log('idx | type | subtopic | d | answer');
+console.log('----+------+-----------------------+---+----------');
+for (const t of TASKS) {
+ console.log(`${String(t.idx).padStart(3)} | ${t.type.padEnd(4)} | ${String(t.subtopic).padEnd(21)} | ${t.diff} | ${String(t.answer)}`);
+}
+
+if (problems.length) {
+ console.error(`\n✗ ПРОБЛЕМЫ (${problems.length}):`);
+ problems.forEach(p => console.error(' - ' + p));
+ console.error('\nЗапись отменена из-за ошибок валидации.');
+ db.close();
+ process.exit(1);
+}
+console.log('\n✓ Валидация и self-check ответов пройдены (30/30).');
+
+/* ── APPLY: upsert ─────────────────────────────────────────────────────────── */
+if (!APPLY) {
+ console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_rt2324_e1v1.js --apply\n');
+ db.close();
+ process.exit(0);
+}
+
+const upsert = db.prepare(`
+ INSERT INTO exam_tasks
+ (exam_key, variant, task_idx, task_type, text_html, figure_html,
+ opts_json, answer, solution_html, topic, subtopic, difficulty)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(exam_key, variant, task_idx) DO UPDATE SET
+ task_type = excluded.task_type,
+ text_html = excluded.text_html,
+ figure_html = excluded.figure_html,
+ opts_json = excluded.opts_json,
+ answer = excluded.answer,
+ solution_html = excluded.solution_html,
+ topic = excluded.topic,
+ subtopic = excluded.subtopic,
+ difficulty = excluded.difficulty
+`);
+
+let n = 0;
+db.exec('BEGIN');
+try {
+ for (const t of TASKS) {
+ upsert.run(
+ EXAM, VARIANT, t.idx, t.type,
+ t.text,
+ t.fig || null,
+ t.type === 'mc' ? JSON.stringify(t.opts) : null,
+ t.answer,
+ buildSolution(t),
+ t.topic, t.subtopic, t.diff
+ );
+ n++;
+ }
+ const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c;
+ db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
+ db.exec('COMMIT');
+ console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}).`);
+ console.log(`✓ exam_tracks.variants_count = ${distinct} (различных вариантов).`);
+ console.log(`\nПробник доступен: /exam-prep/ctmath → «Варианты» → «РТ-2023/24 · этап I».\n`);
+} catch (e) {
+ db.exec('ROLLBACK');
+ console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
+ process.exitCode = 1;
+}
+db.close();
diff --git a/backend/src/routes/exam-prep.js b/backend/src/routes/exam-prep.js
index 079d7d7..2551805 100644
--- a/backend/src/routes/exam-prep.js
+++ b/backend/src/routes/exam-prep.js
@@ -38,6 +38,9 @@ const VARIANT_LABEL = {
101: 'РТ-2024/25 · этап I',
102: 'РТ-2024/25 · этап II',
103: 'РТ-2024/25 · этап III',
+ 104: 'РТ-2023/24 · этап I',
+ 105: 'РТ-2023/24 · этап II',
+ 106: 'РТ-2023/24 · этап III',
},
};
const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;