feat(access): Фаза 1b — управление доступом к симуляциям в админке

Бэкенд /api/access обобщён на тип 'sim': catalog отдаёт симуляции (lab_sims),
summary/matrix/class — карты по всем типам. Админ-секция «Доступ» теперь
показывает «Симуляции» во всех трёх режимах (по контенту / по классу / матрица)
+ поиск; helpers (bucket/keyName/itemsOf) обобщены через карты типов
(CONTENT_TYPES=textbook,exam,sim; course зарезервирован). Теперь админ/учитель
могут открывать/закрывать конкретные симуляции классам и ученикам — закрыт UX-
разрыв из 1a (новые классы без UI-управления). Тест: каталог включает sims; 210 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 13:24:08 +03:00
parent 9a145e5d62
commit 4549b4e819
3 changed files with 64 additions and 47 deletions
+17 -11
View File
@@ -29,7 +29,16 @@ router.get('/catalog', (_req, res) => {
WHERE enabled = 1
ORDER BY sort_order, exam_key
`).all();
res.json({ textbooks, exams });
let sims = [];
try {
sims = db.prepare(`
SELECT id, title, cat AS subject, grade
FROM lab_sims
WHERE enabled = 1
ORDER BY sort_order, id
`).all();
} catch (_e) { /* lab_sims может отсутствовать на старом инстансе — деградация */ }
res.json({ textbooks, exams, sims });
});
/* ── Цели назначения: классы (+ученики) и отдельные ученики ────────────── */
@@ -99,12 +108,9 @@ router.get('/summary', (req, res) => {
AND target_id IN (SELECT id FROM classes WHERE teacher_id = ?)
GROUP BY content_type, content_ref`).all(req.user.id);
const textbooks = {}, exams = {};
for (const r of rows) {
if (r.content_type === 'textbook') textbooks[r.content_ref] = r.n;
else exams[r.content_ref] = r.n;
}
res.json({ totalClasses, textbooks, exams });
const out = { textbook: {}, exam: {}, sim: {}, course: {} };
for (const r of rows) { (out[r.content_type] || (out[r.content_type] = {}))[r.content_ref] = r.n; }
res.json({ totalClasses, textbooks: out.textbook, exams: out.exam, sims: out.sim, courses: out.course });
});
/* ── Матрица класс × контент (обзор и правка одним экраном) ────────────── */
@@ -116,7 +122,7 @@ router.get('/matrix', (req, res) => {
? db.prepare('SELECT id, name FROM classes ORDER BY name').all()
: db.prepare('SELECT id, name FROM classes WHERE teacher_id = ? ORDER BY name').all(req.user.id);
const open = {};
classes.forEach(c => { open[c.id] = { textbook: [], exam: [] }; });
classes.forEach(c => { open[c.id] = { textbook: [], exam: [], sim: [], course: [] }; });
const ids = classes.map(c => c.id);
if (ids.length) {
const ph = ids.map(() => '?').join(',');
@@ -169,9 +175,9 @@ router.get('/class/:id', requireRole('admin', 'teacher'), (req, res) => {
SELECT content_type, content_ref FROM content_access
WHERE scope = 'class' AND target_id = ? AND allow = 1
`).all(cid);
const textbooks = [], exams = [];
for (const r of rows) (r.content_type === 'textbook' ? textbooks : exams).push(r.content_ref);
res.json({ textbooks, exams });
const out = { textbook: [], exam: [], sim: [], course: [] };
for (const r of rows) (out[r.content_type] || (out[r.content_type] = [])).push(r.content_ref);
res.json({ textbooks: out.textbook, exams: out.exam, sims: out.sim, courses: out.course });
});
function teacherCanManageStudent(teacherId, studentId) {
const inClass = db.prepare(`