From f26b5222077c474ab50690d3a313840c51a4b038 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 13 Jun 2026 13:25:24 +0300 Subject: [PATCH] =?UTF-8?q?feat(sim-builder):=20=D1=84=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=207=20=E2=80=94=20custom-sim=20=D0=BD=D0=B0=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D0=BA=D0=B5=20=D0=BE=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-?= =?UTF-8?q?=D1=83=D1=80=D0=BE=D0=BA=D0=B0=20(=D1=81=D0=B8=D0=BD=D1=85?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=83,=20?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D0=BE=D1=82=D0=B0=D1=86=D0=B8=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 9 ++++ backend/src/controllers/classroom/sim.js | 21 +++++++- frontend/classroom.html | 31 ++++++++++-- frontend/js/labs/lab-glue.js | 48 +++++++++++++++++- plans/sim-builder/CONTEXT.md | 41 +++++++++++++-- plans/sim-builder/PLAN.md | 4 +- plans/sim-builder/phase-7-classroom.md | 64 +++++++++++++++++++----- 7 files changed, 192 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cdbf5a7..2ed5705 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,3 +124,12 @@ git push origin master - **publish-toggle через PUT status.** Снять с публикации = `customSimUpdate(id, { status:'draft' })` (контроллер Ф3 уже принимает `status` в update). В билдере для уже сохранённой sim — `setStatus` (без полного save, не бампит version зря); в каталоге — кнопка publish/unpublish на owner-карточке. - **clone-источник:** своя любая ИЛИ чужая published (чужой draft → 403). Кнопка «Клонировать к себе» — только на чужой published-карточке и только для teacher/admin (`_isTeacherUser()`). Копируется `spec_json` как есть (уже санитизирован при сохранении оригинала), status=draft, version=1, title += ' (копия)'. - **Аддитивность сохранена**: lab-glue.js правлен только внутри IIFE `LabCustom` (ICON-блок + `_cardHtml` actions + делегат + 3 новые функции + экспорт); lab.html/classroom.html не тронуты. Кнопки — inline-стиль + SVG `.ic`, без эмодзи. + +### Phase 7 — Learnings + +- **Доска грузит sim в IFRAME, НЕ монтирует движок напрямую.** Ключевое открытие: `onSimOpen(simId)` в classroom.html просто ставит `cr-sim-frame.src = /lab?embed=1&sim=`. Значит custom-sim на доску = переиспользование Ф5-пути: iframe `/lab?embed=1&sim=custom:` сам монтирует SimEngine через `LabCustom.init→openSim→registerSpecSim`. Никакого прямого `SimEngine.mount` в классруме — план («смонтировать SimEngine в container доски») был неточен, фактический конвейер чище. +- **Синхрон состояния — обобщённый мост `sim_state`/`apply_sim_state` (postMessage), НЕ per-sim код в классруме.** Каждая встроенная sim в embed зовёт `_registerSimState(id, getState, applyState)` + `_startStateEmit(id)` (lab-glue.js, top-level). Учительский iframe постит `{type:'sim_state',state}` родителю → classroom relay `POST /sim/state` → SSE → ученик постит `{type:'apply_sim_state',state}` в свой iframe → `_simStateRegistry[_autoSim].applyState`. Custom-sim просто подключается к тому же реестру: `_bridgeCustomSimState(real)` с getState=`{params,running}` / applyState=`setParam`+play/pause поверх `real.instance()` (SimEngine: `.params`, `setParam`, `isRunning`, `play`, `pause`). +- **Ключ реестра состояния = `_autoSim` (raw `custom:`), НЕ реестровый id.** Обработчик `apply_sim_state` берёт `_simStateRegistry[_autoSim]`, а `_autoSim` — это сырой URL-param `custom:` (двоеточие!), хотя в LabRegistry sim лежит под `customsim_` (resolveId). Регистрировать мост надо под `_autoSim`, иначе ученик не применит state. Гоча неочевидная. +- **simId с двоеточием ломал бэкенд-валидацию.** `simOpen` валидировал `^[a-z0-9_-]{1,40}$` — двоеточие в `custom:5` не проходило. Добавлена ветка `^custom:(\d+)$` + проверка доступа (own|published|admin → иначе 404/403). Доступ дублируется на `GET /custom-sims/:id` (ensureSpec в iframe) — две линии обороны, чужой draft не утечёт. +- **Закрытие = `frame.src='about:blank'` сносит весь iframe-документ** (SimEngine, rAF, listeners, `_simStateRegistry`) — явный `destroy()` в классруме не нужен, чисто по построению. Смена sim — тот же сброс src + новый load. +- **classroom.html (8240 строк) — искать через vex по DOM-id** (`cr-sim-picker-grid`, `cr-sim-frame`), затем точечный Read. ast-index НЕ индексирует inline-`