8.5 KiB
8.5 KiB
Phase 6: Раздача / шаблоны / клон / программа
Status: ✅ Implemented (pending commit — за оркестратором) Parent plan: PLAN.md Domain: fullstack
Objective
Экосистема вокруг custom-sims: публикация, раздача классу, клонирование чужих, старт из шаблонов, привязка к программе (учебник/тема).
Tasks
- Публикация: тумблер draft↔published. В билдере — кнопка «Опубликовать»/«Снять» (PUT status,
setStatus); в каталоге — кнопки на owner-карточке (publish/unpublish). Только владелец/admin. - Раздача классу:
POST /api/custom-sims/:id/share { classId }(requireRole teacher,admin + per-row ownership). РЕШЕНИЕ: published custom-sim И ТАК видна всем в каталоге, поэтому раздача = авто-публикация (status→published) + ДОЛГОВЕЧНОЕ уведомление ученикам класса (pushNotif, notifications-таблица + SSE) со ссылкой/lab?sim=custom:<id>. БЕЗ копии и БЕЗ content_access (custom-sim не гейтится allowlist'ом 'sim' — тот гейтит только legacy lab_sims). - Клон:
POST /api/custom-sims/:id/clone— копия spec вызвавшему как draft, title += « (копия)», version=1. Источник: своя (любая) ИЛИ чужая published. Кнопка «Клонировать к себе» на чужой published-карточке (только для teacher/admin) → ведёт на/sim-builder?id=<newId>. - Шаблоны: 4 встроенных спеки в
TEMPLATES(sim-builder.js): пустая, маятник, график y=f(x), бросок. Кнопка «Шаблон» в тулбаре → модалка выбора →loadFromSimкак новая симуляция. «Создать из существующей» = clone (с чужой карточки). - Привязка к программе: переиспользован
lab_sim_linksсsim_id='custom:<id>'(sim_id — TEXT, отдельная таблица не нужна). Эндпоинты на роутере custom-sims: GET/:id/related, POST/:id/links, DELETE/:id/links/:linkId(владелец/admin управляет связями СВОЕЙ симуляции). Клиент:customSimRelated/AddLink/DelLink. Backend + чтение готовы; UI-редактор связей в билдере и чипы в каталоге — НЕ сделаны (см. Handoff: остаток). - Тесты:
backend/tests/custom-sims-share.test.js(15 it): share (авто-publish + durable-уведомление, 400/403/404), clone (новый владелец/draft/spec, чужой published OK, чужой draft 403), links (привязка/удаление/related, ownership). seedRow-паттерн (class/members/textbook).
Files to Modify/Create
backend/src/controllers/customSimController.js— share/clone/publish (modify)backend/src/routes/customSims.js— роуты share/clone (modify)backend/src/controllers/.../lab links— связи для custom (reuselab.jslinks, modify при необходимости)frontend/sim-builder.html/frontend/js/labs/lab-glue.js— шаблоны, кнопки публикации/клона/раздачи (modify)js/api.js— методы share/clone (modify)backend/tests/custom-sims-share.test.js(new)
Acceptance Criteria
- Учитель публикует; другой учитель видит и клонирует к себе (draft).
- Выданная классу симуляция доступна ученикам класса (с уведомлением).
- Старт из шаблона создаёт рабочую заготовку. Привязка к учебнику показывает чип/кнопку.
Notes
- Раздача — переиспользовать существующий механизм доступа/уведомлений, не строить новый.
- Решение копия-vs-ссылка зафиксировать в CONTEXT.md.
Review Checklist
- Все задачи выполнены (привязка к программе — backend+чтение; UI-редактор связей в Handoff)
- Ownership на publish/share/clone/links покрыт тестами (чужой draft → 403, чужой published clone → ОК)
- Ученик класса получает уведомление; не-владелец/не-свой-класс → 403
- Reuse материалов/доступа/links, без дублей (pushNotif, lab_sim_links, паттерн share из materials)
Handoff to Next Phase
Что реализовано (Ф6)
- Backend (
customSimController.js+customSims.js):POST /:id/share { classId }— авто-publish +pushNotif(uid,'sim_shared',msg,'/lab?sim=custom:<id>')каждому ученику класса. Возвращает{ ok, sent, status:'published' }. requireRole(teacher,admin) + ownership симуляции + проверкаclasses.teacher_id.POST /:id/clone→ 201{ id }. Источник: своя (любая) ИЛИ чужая published. status=draft, version=1, title += ' (копия)', spec_json копируется как есть.GET /:id/related(auth, own/published) /POST /:id/links/DELETE /:id/links/:linkId(owner/admin) — поверхlab_sim_links,sim_id='custom:<id>'. DELETE симуляции чистит её связи.- Решение копия-vs-доступ: доступ (published виден всем) + уведомление; НЕ копия, НЕ content_access.
- Клиент (
js/api.js):customSimShare/Clone/Related/AddLink/DelLinkвwindow.LS. - Каталог (
lab-glue.js, IIFELabCustom, аддитивно): owner-карточка — кнопки Раздать классу / Опубликовать↔Снять; чужая published (для teacher/admin) — «Клонировать к себе». Делегатdata-actрасширен (share/clone/publish/unpublish). Модалка выбора класса (LS.getClasses+LS.modal). Публичное API: добавленыLabCustom.share/clone/setStatus. - Билдер (
sim-builder.js): тулбар — «Шаблон» (TEMPLATES×4: пустая/маятник/график/бросок →loadFromSimкак новая), «Раздать» (для сохранённой), publish-toggle (Опубликовать↔Снять). Новые методы:setStatus,openShareModal,openTemplateModal. ICON.template/unpublish.
Остаток (минимум по плану — не сделано, низкий приоритет)
- UI-редактор курикулумных связей в билдере (select учебника из
/api/access/catalog+ добавить/ удалить) и чипы «Связано с программой» в каталоге/на странице sim. Backend и/relatedготовы — это чисто фронтовая надстройка. Обратный поиск «какие custom-sim привязаны к учебнику» (как/api/lab/links?kind=textbook&ref_id=) для custom НЕ добавлен:/api/lab/linksджойнитlab_sims, а у custom строк там нет — при желании добавить отдельный bulk-эндпоинт или LEFT JOIN на custom_sims.
Для Ф7 (доска онлайн-урока)
- Источник sim для доски:
LS.customSimGet(id)→sim.spec; рендер —window.SimEngine.mount(host, spec)(как в билдере/каталоге). Deep-link/идентификатор:custom:<dbid>; в LabRegistry —customsim_<dbid>(resolveId). published-симуляция доступна всем (для учеников на доске — без доп. прав). - Раздача классу уже шлёт уведомление со ссылкой
/lab?...— на доске ссылку при необходимости заменить на классную сессию; механизмpushNotifпереиспользуем.