# Phase 7: Доска онлайн-урока **Status:** ✅ Implemented (pending commit) **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** fullstack ## Objective Открывать custom-симуляцию на доске онлайн-урока через существующий конвейер встраивания sim, с синхроном параметров классу и аннотациями поверх. ## Tasks - [x] Учитель в classroom выбирает sim для доски: добавить в список источников свои+published custom-sims (рядом со встроенными). → `_crLoadCustomSims()` (`LS.customSimsList`) + мёрж в `_crRenderSimGrid`; карточка с бейджем «Моя», id=`custom:`. - [x] Открытие на доске через существующий `simOpen` (controller `classroom/sim.js`, роут `/:id/sim`) — для custom передаётся `custom:`. Рантайм НЕ монтируется напрямую в доску: доска уже грузит sim в **iframe** `/lab?embed=1&sim=...`; для custom это `/lab?embed=1&sim=custom:`, где `LabCustom`/ `registerSpecSim` монтирует `SimEngine` (путь Ф5). Конвейер iframe переиспользован 1:1. - [x] Синхрон состояния: параметры + play/pause транслируются классу через существующий мост `sim_state`/`apply_sim_state` (lab-glue) → `simState`. Custom-sim подключён к мосту через `_bridgeCustomSimState(real)` (getState/applyState поверх `SimEngine.params`/`setParam`/`play`/`pause`). ⚠️ **Время t (фаза анимации) жёстко НЕ синхронится** — только параметры слайдеров + признак running. - [x] Аннотации поверх — через существующий `simAnnotate` (без изменений конвейера, id-agnostic). - [x] Закрытие/смена sim: `onSimClose` сбрасывает `frame.src='about:blank'` → весь документ iframe (SimEngine + rAF + слушатели + реестр состояния) сносится вместе с iframe. Утечек нет. - [x] Проверка: логическая (трасса teacher→backend→SSE→student) + node --check + npm test (в baseline). Headless classroom тяжёл; синхрон проверен по конвейеру, открытие/доступ/закрытие — по коду. ## Files to Modify/Create - `frontend/classroom.html` — выбор custom-sim в источниках доски, монтаж SimEngine, проброс состояния (modify) - `backend/src/controllers/classroom/sim.js` — поддержать `custom:` (валидация доступа к published/own) (modify) - `js/api.js` — при необходимости (modify, опц.) ## Acceptance Criteria - Учитель открывает custom-sim на доске; ученики видят её синхронно (параметры/анимация/режим). - Аннотации поверх работают; закрытие чистит ресурсы. - Существующее встраивание встроенных sim не регрессировало. ## Notes - Reuse simOpen/simState/simMode/simAnnotate — НЕ строить новый канал синхрона. - Доступ: на доску можно класть только свои или published (проверка на сервере). - classroom.html большой (8240 строк) — править точечно. ## Review Checklist - [x] Все задачи выполнены - [x] Синхрон параметров учитель→ученики работает (sim_state мост, params+running) - [x] Доступ к custom-sim на доске проверяется (server simOpen: own|published|admin; иначе 403/404) - [x] Встроенные sim на доске не сломаны (id-ветка для встроенных без изменений); ресурсы чистятся через about:blank ## Handoff to Next Phase ## Handoff — итог Ф7 (степень синхрона, что осталось) **Изменённые файлы (минимально, аддитивно):** - `backend/src/controllers/classroom/sim.js` (+21/-2): `simOpen` принимает `custom:`, валидирует доступ (владелец ИЛИ published ИЛИ admin → иначе 404/403). Встроенный id — старый regex. `simState/simMode/simAnnotate/simClose` НЕ менялись (state-объект и так произвольный, ≤64KB). - `frontend/classroom.html` (+31/-4): `_crLoadCustomSims()` (кэш `LS.customSimsList`), `crOpenSimPicker` async + предзагрузка, `_crRenderSimGrid` мёржит custom (бейдж «Моя», id `custom:`, XSS-escape заголовков/id в onclick). `crPickSim` уже передаёт id как есть. - `frontend/js/labs/lab-glue.js` (+48/-1): `_bridgeCustomSimState(real)` — мост состояния для custom-sim; вызывается в `_registerLazy.open()` после `real.open(ctx)` (только embed). Регистрирует `getState={params,running}` / `applyState` (setParam+play/pause) под ключом `_autoSim` (`custom:`), запускает `_startStateEmit`. Тем же `_simStateRegistry`/каналом, что встроенные. **Степень синхрона:** параметры слайдеров + признак воспроизведения (play/pause) — ПОЛНЫЙ синхрон учитель→ученики (demo-режим). Время `t` (фаза анимации) НЕ синхронизируется покадрово: ученик запускает свой rAF при running=true, фаза может разъезжаться. Достаточно по требованиям фазы. При желании в будущем: добавить `t` в state и `inst._t` сеттер (потребует расширения SimEngine API). **Открытие на доске:** через iframe `/lab?embed=1&sim=custom:` (НЕ прямой mount в доску) — переиспользован существующий конвейер; SimEngine монтируется уже внутри iframe (путь Ф5). **Доступ:** двойная проверка — (1) `simOpen` на сервере при постановке на доску; (2) `GET /api/custom-sims/:id` (ensureSpec) при загрузке спеки в iframe. Чужой draft → 403 на обоих. **Что осталось / риски:** - Нет интеграционного теста classroom-сессии (нет харнесса; добавление потребовало бы мока сессий и риска зайти в зону параллельной сессии). Проверка — логическая + node --check + npm test (в baseline). - Время анимации не синхронится покадрово (см. выше) — by design. - Остаток Ф6 (UI-редактор связей в билдере + чипы в каталоге) — вне Ф7.