diff --git a/backend/src/routes/exam-prep.js b/backend/src/routes/exam-prep.js
index 112c8c8..629a053 100644
--- a/backend/src/routes/exam-prep.js
+++ b/backend/src/routes/exam-prep.js
@@ -1,7 +1,7 @@
'use strict';
const router = require('express').Router();
const db = require('../db/db');
-const { authMiddleware } = require('../middleware/auth');
+const { authMiddleware, requireRole } = require('../middleware/auth');
const access = require('../services/contentAccess');
router.use(authMiddleware);
@@ -416,6 +416,28 @@ router.get('/tracks', (req, res) => {
res.json({ tracks });
});
+/* ── Админ: управление экзамен-модулями (вкл/выкл) ──
+ Отдельные пути (без :examKey, чтобы не задеть гейт content_access). */
+router.get('/admin/tracks', requireRole('admin'), (_req, res) => {
+ const tracks = db.prepare(`
+ SELECT exam_key, title, subject_slug, grade, enabled, variants_count, sort_order,
+ (SELECT COUNT(*) FROM exam_tasks t WHERE t.exam_key = exam_tracks.exam_key) AS task_count
+ FROM exam_tracks
+ ORDER BY sort_order, exam_key
+ `).all();
+ res.json({ tracks });
+});
+
+router.patch('/admin/track', requireRole('admin'), (req, res) => {
+ const key = String(req.body && req.body.exam_key || '').trim();
+ if (!key) return res.status(400).json({ error: 'exam_key required' });
+ if (!db.prepare('SELECT 1 FROM exam_tracks WHERE exam_key = ?').get(key))
+ return res.status(404).json({ error: 'Unknown exam track' });
+ const enabled = req.body && req.body.enabled ? 1 : 0;
+ db.prepare('UPDATE exam_tracks SET enabled = ? WHERE exam_key = ?').run(enabled, key);
+ res.json({ ok: true, exam_key: key, enabled });
+});
+
/* ── GET /api/exam-prep/:examKey/info ──
Track metadata + global counts + this user's aggregate progress. */
// @public-by-design: router-level authMiddleware (line 6) covers this route
diff --git a/frontend/admin.html b/frontend/admin.html
index 2ccb7b5..25803df 100644
--- a/frontend/admin.html
+++ b/frontend/admin.html
@@ -1067,6 +1067,9 @@
+
@@ -1616,6 +1619,16 @@
+
+
+
Экзамен-модули
+
+ Включение/выключение модулей подготовки к экзамену (/exam-prep). Выключенный модуль
+ скрыт у учеников и не показывается в каталоге прав доступа. Доступ ученикам открывается отдельно
+ в разделе «Доступ · контент» → «Экзамены».
+
+
+
Доступ к учебникам и экзаменам
@@ -2136,6 +2149,7 @@
+
diff --git a/frontend/js/admin/admin.js b/frontend/js/admin/admin.js
index aeef22b..c6a0bae 100644
--- a/frontend/js/admin/admin.js
+++ b/frontend/js/admin/admin.js
@@ -15,7 +15,7 @@
AdminCtx.isAdmin = isAdmin;
/* Admin-only tabs: show to everyone for discoverability, but lock for non-admins */
- const ADMIN_ONLY_TABS = ['btn-tab-subjects','btn-tab-permissions','btn-tab-shop','btn-tab-gam','btn-tab-tpl','btn-tab-sims','btn-tab-games','btn-tab-assistant','btn-tab-imggen'];
+ const ADMIN_ONLY_TABS = ['btn-tab-subjects','btn-tab-permissions','btn-tab-shop','btn-tab-gam','btn-tab-tpl','btn-tab-sims','btn-tab-exams','btn-tab-games','btn-tab-assistant','btn-tab-imggen'];
const lockSvg = '';
ADMIN_ONLY_TABS.forEach(id => {
const el = document.getElementById(id);
@@ -64,6 +64,7 @@
gam: 'gam',
tpl: 'tpl',
sims: 'sims',
+ exams: 'exams',
games: 'games',
assistant: 'assistant',
imggen: 'imggen',
diff --git a/frontend/js/admin/sections/exams.js b/frontend/js/admin/sections/exams.js
new file mode 100644
index 0000000..93d5a42
--- /dev/null
+++ b/frontend/js/admin/sections/exams.js
@@ -0,0 +1,72 @@
+'use strict';
+/* admin → exams (exam-prep modules) section.
+ * Список ВСЕХ экзамен-треков (вкл. выключенные) + тумблер enabled.
+ * Источник: GET /api/exam-prep/admin/tracks; переключение: PATCH /api/exam-prep/admin/track.
+ * Влияет на видимость модуля в /exam-prep и в каталоге прав доступа (Экзамены). */
+(function () {
+ 'use strict';
+ let inited = false;
+ let _tracks = [];
+
+ const SUBJ = { math: 'Математика', physics: 'Физика', phys: 'Физика', chemistry: 'Химия',
+ chem: 'Химия', biology: 'Биология', bio: 'Биология' };
+ function esc(s) {
+ return String(s == null ? '' : s).replace(/[&<>"']/g, c =>
+ ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+ }
+
+ async function load() {
+ try {
+ const data = await LS.api('/api/exam-prep/admin/tracks');
+ _tracks = Array.isArray(data.tracks) ? data.tracks : [];
+ _render();
+ } catch (e) { LS.toast('Ошибка загрузки экзамен-модулей: ' + e.message, 'error'); }
+ }
+
+ function _render() {
+ const grid = document.getElementById('exams-grid');
+ if (!grid) return;
+ if (!_tracks.length) { grid.innerHTML = '