feat(admin): тумблер вкл/выкл для экзамен-модулей (exam-prep)

Не было UI для управления exam_tracks.enabled (только флаг в БД, ставился
миграцией). Добавлена админ-секция «Экзамен-модули»:
- backend exam-prep.js: GET /admin/tracks (все треки, вкл. выключенные, + число
  заданий) и PATCH /admin/track (exam_key, enabled), обе requireRole('admin').
  Пути без :examKey, чтобы не задеть гейт content_access.
- frontend: секция sections/exams.js (список треков + переключатель enabled),
  вкладка в admin.html (admin-only через ADMIN_ONLY_TABS, locked для не-админов),
  регистрация в admin.js (ROUTE_TO_SECTION).

Выключенный трек скрыт у учеников и пропадает из каталога прав доступа (тот
берёт exam_tracks WHERE enabled=1). Доступ ученикам по-прежнему в «Доступ · контент».
Требует перезапуска бэкенда + Ctrl+F5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-15 12:32:01 +03:00
parent 1cf8083c0e
commit 6fed18f819
4 changed files with 111 additions and 2 deletions
+23 -1
View File
@@ -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