feat(lab-content-engine): phase 5 завершение — редактор связей в админке + кнопка в учебнике

- lab.js: GET /api/lab/links/all?kind= — пакетный обратный поиск (byRef map),
  чтобы каталог учебников не делал N+1 запросов
- tests/lab-links.test.js: +3 теста для /links/all (group/400/401) -> 21/21
- admin/sections/sims.js: inline-редактор курикулумных связей на карточке симуляции
  (кнопка «Связи» -> панель: список связей с удалением + выбор учебника + добавить);
  использует /api/access/catalog, POST/DELETE /links. Без LS.modal (inline-панель)
- textbooks.html: кнопка «В лабораторию» на карточке учебника, если есть связанные
  симуляции (один батч-запрос /links/all при загрузке); deep-link /lab?sim=<id>

Двусторонняя навигация sim <-> учебник готова. Иконки .ic, без эмодзи.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 17:26:35 +03:00
parent 6b0d556347
commit fb6175e4a2
3 changed files with 156 additions and 8 deletions
+18
View File
@@ -148,6 +148,24 @@ describe('/api/lab curriculum links', () => {
await inject('PATCH', '/api/lab/sims/pendulum', { enabled: true }, adminToken); // restore
});
it('batch reverse lookup: GET /links/all?kind=textbook groups by ref_id', async () => {
const res = await inject('GET', '/api/lab/links/all?kind=textbook', null, studentToken);
assert.equal(res.status, 200, `got ${res.status}`);
assert.ok(res.body.byRef, 'byRef present');
assert.ok(Array.isArray(res.body.byRef[tbSlug]), `byRef[${tbSlug}] is array`);
assert.ok(res.body.byRef[tbSlug].some(s => s.id === 'pendulum'), 'pendulum grouped under tbSlug');
});
it('batch reverse lookup: bad kind → 400', async () => {
const res = await inject('GET', '/api/lab/links/all?kind=nope', null, studentToken);
assert.equal(res.status, 400);
});
it('batch reverse lookup requires auth (401)', async () => {
const res = await inject('GET', '/api/lab/links/all?kind=textbook', null, null);
assert.equal(res.status, 401);
});
it('reverse lookup: bad kind → 400', async () => {
const res = await inject('GET', '/api/lab/links?kind=nope&ref_id=x', null, studentToken);
assert.equal(res.status, 400);