91 lines
8.5 KiB
Markdown
91 lines
8.5 KiB
Markdown
# Phase 6: Раздача / шаблоны / клон / программа
|
||
|
||
**Status:** ✅ Implemented (pending commit — за оркестратором)
|
||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||
**Domain:** fullstack
|
||
|
||
## Objective
|
||
Экосистема вокруг custom-sims: публикация, раздача классу, клонирование чужих,
|
||
старт из шаблонов, привязка к программе (учебник/тема).
|
||
|
||
## Tasks
|
||
- [x] Публикация: тумблер draft↔published. В билдере — кнопка «Опубликовать»/«Снять» (PUT status,
|
||
`setStatus`); в каталоге — кнопки на owner-карточке (publish/unpublish). Только владелец/admin.
|
||
- [x] Раздача классу: `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).
|
||
- [x] Клон: `POST /api/custom-sims/:id/clone` — копия spec вызвавшему как draft, title += « (копия)»,
|
||
version=1. Источник: своя (любая) ИЛИ чужая published. Кнопка «Клонировать к себе» на чужой
|
||
published-карточке (только для teacher/admin) → ведёт на `/sim-builder?id=<newId>`.
|
||
- [x] Шаблоны: 4 встроенных спеки в `TEMPLATES` (sim-builder.js): пустая, маятник, график y=f(x),
|
||
бросок. Кнопка «Шаблон» в тулбаре → модалка выбора → `loadFromSim` как новая симуляция.
|
||
«Создать из существующей» = clone (с чужой карточки).
|
||
- [x] Привязка к программе: переиспользован `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: остаток).
|
||
- [x] Тесты: `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 (reuse `lab.js` links, 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
|
||
- [x] Все задачи выполнены (привязка к программе — backend+чтение; UI-редактор связей в Handoff)
|
||
- [x] Ownership на publish/share/clone/links покрыт тестами (чужой draft → 403, чужой published clone → ОК)
|
||
- [x] Ученик класса получает уведомление; не-владелец/не-свой-класс → 403
|
||
- [x] 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`, IIFE `LabCustom`, аддитивно): 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` переиспользуем.
|