feat(sim-builder): фаза 7 — custom-sim на доске онлайн-урока (синхрон параметров классу, аннотации)
This commit is contained in:
@@ -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=<simId>`. Значит custom-sim на доску = переиспользование Ф5-пути: iframe `/lab?embed=1&sim=custom:<id>` сам монтирует 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:<dbid>`), НЕ реестровый id.** Обработчик `apply_sim_state` берёт `_simStateRegistry[_autoSim]`, а `_autoSim` — это сырой URL-param `custom:<dbid>` (двоеточие!), хотя в LabRegistry sim лежит под `customsim_<dbid>` (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-`<script>` в HTML (символы `crOpenSimPicker` и т.п. → пусто); vex тоже не парсит тела inline-функций. Для тел функций в HTML — Grep tool (документированный escape-hatch ast-index.md: «ONLY when ast-index returns empty»). Проверка инлайна: извлечь `<script>` без src в temp .js → `node --check` → удалить.
|
||||
|
||||
@@ -10,10 +10,27 @@ function simOpen(req, res) {
|
||||
return res.status(403).json({ error: 'Нет доступа' });
|
||||
|
||||
const { simId, title } = req.body;
|
||||
if (!simId || typeof simId !== 'string' || !/^[a-z0-9_-]{1,40}$/.test(simId))
|
||||
if (!simId || typeof simId !== 'string')
|
||||
return res.status(400).json({ error: 'Неверный simId' });
|
||||
|
||||
emitToSession(sessionId, { type: 'classroom_sim_open', sessionId, simId, title: (title || simId).slice(0, 80) });
|
||||
// Конструктор симуляций (Фаза 7): custom-симуляция в формате 'custom:<dbid>'.
|
||||
// На доску можно класть только СВОЮ симуляцию (владелец) ИЛИ published
|
||||
// (доступную всем) — проверяем на сервере, draft чужого не пройдёт.
|
||||
let resolvedTitle = title;
|
||||
const custom = /^custom:(\d+)$/.exec(simId);
|
||||
if (custom) {
|
||||
const cid = Number(custom[1]);
|
||||
const sim = db.prepare('SELECT id, owner_id, status, title FROM custom_sims WHERE id=?').get(cid);
|
||||
if (!sim) return res.status(404).json({ error: 'Симуляция не найдена' });
|
||||
if (sim.owner_id !== req.user.id && sim.status !== 'published' && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Нет доступа к симуляции' });
|
||||
}
|
||||
if (!resolvedTitle) resolvedTitle = sim.title || 'Симуляция';
|
||||
} else if (!/^[a-z0-9_-]{1,40}$/.test(simId)) {
|
||||
return res.status(400).json({ error: 'Неверный simId' });
|
||||
}
|
||||
|
||||
emitToSession(sessionId, { type: 'classroom_sim_open', sessionId, simId, title: (resolvedTitle || simId).slice(0, 80) });
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
+26
-5
@@ -7056,14 +7056,32 @@
|
||||
const CAT_LABELS = { math:'Математика', phys:'Физика', chem:'Химия', bio:'Биология', game:'Игра' };
|
||||
|
||||
let _simPickerCat = 'all'; // active filter in picker
|
||||
// Конструктор симуляций (Фаза 7): свои + published custom-симуляции для доски.
|
||||
let _crCustomSims = null; // [{ id, cat, title, _custom:true }] — кэш списка
|
||||
|
||||
function crOpenSimPicker() {
|
||||
async function _crLoadCustomSims() {
|
||||
if (_crCustomSims) return _crCustomSims;
|
||||
try {
|
||||
const data = await LS.customSimsList();
|
||||
const rows = (data && data.sims) || [];
|
||||
_crCustomSims = rows.map(s => ({
|
||||
id: 'custom:' + s.id,
|
||||
cat: s.cat || 'phys',
|
||||
title: s.title || ('Симуляция #' + s.id),
|
||||
_custom: true,
|
||||
}));
|
||||
} catch (e) { _crCustomSims = []; }
|
||||
return _crCustomSims;
|
||||
}
|
||||
|
||||
async function crOpenSimPicker() {
|
||||
if (_simActive) {
|
||||
// If sim already open — clicking "Симуляция" closes it (teacher action)
|
||||
crTeacherCloseSim();
|
||||
return;
|
||||
}
|
||||
_simPickerCat = 'all';
|
||||
await _crLoadCustomSims();
|
||||
_crRenderSimGrid('all');
|
||||
const overlay = document.getElementById('cr-sim-picker-overlay');
|
||||
overlay.classList.add('open');
|
||||
@@ -7084,11 +7102,14 @@
|
||||
|
||||
function _crRenderSimGrid(cat) {
|
||||
const grid = document.getElementById('cr-sim-picker-grid');
|
||||
const sims = cat === 'all' ? CR_SIMS : CR_SIMS.filter(s => s.cat === cat);
|
||||
// Конструктор симуляций (Фаза 7): встроенные + свои/published custom-sims.
|
||||
const all = CR_SIMS.concat(_crCustomSims || []);
|
||||
const sims = cat === 'all' ? all : all.filter(s => s.cat === cat);
|
||||
const esc = v => String(v == null ? '' : v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
grid.innerHTML = sims.map(s => `
|
||||
<div class="cr-sim-picker-card" onclick="crPickSim('${s.id}','${s.title.replace(/'/g,'\\\'')}')" title="${s.title}">
|
||||
<span class="cr-sim-picker-card-cat ${s.cat}">${CAT_LABELS[s.cat] || s.cat}</span>
|
||||
<span class="cr-sim-picker-card-title">${s.title}</span>
|
||||
<div class="cr-sim-picker-card" onclick="crPickSim('${String(s.id).replace(/'/g,"\\'")}','${esc(s.title).replace(/'/g,"\\'")}')" title="${esc(s.title)}">
|
||||
<span class="cr-sim-picker-card-cat ${s.cat}">${s._custom ? 'Моя' : (CAT_LABELS[s.cat] || s.cat)}</span>
|
||||
<span class="cr-sim-picker-card-title">${esc(s.title)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -395,6 +395,45 @@ const SIMS = [
|
||||
_lastEmittedState = null;
|
||||
}
|
||||
|
||||
/* Конструктор симуляций (Фаза 7): подключить custom-sim (SimEngine-инстанс через
|
||||
адаптерный манифест real.instance()) к тому же мосту sim_state/apply_sim_state,
|
||||
что и встроенные. Состояние = { params, running } — параметры слайдеров +
|
||||
признак воспроизведения. applyState проигрывает их у ученика через setParam/
|
||||
play/pause (время жёстко не синхронится — параметры и play/pause достаточны).
|
||||
Регистрируем под ключом _autoSim ('custom:<dbid>'), т.к. обработчик
|
||||
apply_sim_state у ученика берёт _simStateRegistry[_autoSim]. */
|
||||
function _bridgeCustomSimState(real) {
|
||||
if (!_embedMode || !real || typeof real.instance !== 'function') return;
|
||||
var key = _autoSim;
|
||||
if (!key || _simStateRegistry[key]) return; // уже подключено
|
||||
function getState() {
|
||||
var inst = real.instance();
|
||||
if (!inst || !inst.params) return null;
|
||||
var p = {};
|
||||
for (var k in inst.params) {
|
||||
if (Object.prototype.hasOwnProperty.call(inst.params, k)) {
|
||||
var v = inst.params[k];
|
||||
if (typeof v === 'number' && isFinite(v)) p[k] = v;
|
||||
}
|
||||
}
|
||||
return { params: p, running: !!(inst.isRunning && inst.isRunning()) };
|
||||
}
|
||||
function applyState(st) {
|
||||
var inst = real.instance();
|
||||
if (!inst || !st) return;
|
||||
if (st.params) {
|
||||
for (var k in st.params) {
|
||||
if (Object.prototype.hasOwnProperty.call(st.params, k)) inst.setParam(k, st.params[k]);
|
||||
}
|
||||
}
|
||||
var run = !!st.running, isRun = !!(inst.isRunning && inst.isRunning());
|
||||
if (run && !isRun && inst.play) inst.play();
|
||||
else if (!run && isRun && inst.pause) inst.pause();
|
||||
}
|
||||
_registerSimState(key, getState, applyState);
|
||||
_startStateEmit(key);
|
||||
}
|
||||
|
||||
// Receive apply_sim_state from parent (students)
|
||||
window.addEventListener('message', e => {
|
||||
if (!_embedMode) return;
|
||||
@@ -683,7 +722,14 @@ const SIMS = [
|
||||
real._custom = true;
|
||||
real._customId = dbid;
|
||||
if (window.LabRegistry) window.LabRegistry.setActive(real);
|
||||
return real.open(ctx);
|
||||
var _r = real.open(ctx);
|
||||
// Конструктор симуляций (Фаза 7): синхрон параметров/play на доске
|
||||
// онлайн-урока. В embed подключаем custom-sim к общему мосту
|
||||
// sim_state/apply_sim_state — тем же каналом, что и встроенные.
|
||||
// Ключ — исходный _autoSim ('custom:<dbid>'), т.к. apply_sim_state
|
||||
// у ученика берёт _simStateRegistry[_autoSim].
|
||||
try { _bridgeCustomSimState(real); } catch (e) {}
|
||||
return _r;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
# Feature Context: Конструктор симуляций (SimForge)
|
||||
|
||||
## Current State
|
||||
- **ВСЕ ФАЗЫ (0–7) РЕАЛИЗОВАНЫ** (в рабочем дереве, не закоммичено — коммит за оркестратором).
|
||||
Фича «Конструктор симуляций» функционально полна: рантайм+физика, БД+API, билдер, каталог в /lab,
|
||||
раздача/клон/шаблоны/привязка, доска онлайн-урока с синхроном классу.
|
||||
- **Фаза 7 РЕАЛИЗОВАНА** (в рабочем дереве, не закоммичено). Custom-sim на доске онлайн-урока через
|
||||
существующий iframe-конвейер. **Аддитивные** правки трёх файлов; рабочее дерево по ним было ЧИСТЫМ
|
||||
до начала (classroom.html не имел чужих незакоммиченных правок параллельной сессии — проверено git status).
|
||||
- `backend/src/controllers/classroom/sim.js` (+21/-2): `simOpen` принимает `simId='custom:<dbid>'`,
|
||||
валидирует доступ (владелец ИЛИ published ИЛИ admin; иначе 404/403). Встроенный id — прежний regex
|
||||
`^[a-z0-9_-]{1,40}$`. `simState/simMode/simAnnotate/simClose` НЕ тронуты (state-объект уже произвольный).
|
||||
- `frontend/classroom.html` (+31/-4): `_crLoadCustomSims()` (кэш `LS.customSimsList`), `crOpenSimPicker`
|
||||
async с предзагрузкой, `_crRenderSimGrid` мёржит свои+published custom (бейдж «Моя», id `custom:<dbid>`,
|
||||
XSS-escape). Существующий `crPickSim` передаёт id как есть; `onSimOpen` грузит iframe
|
||||
`/lab?embed=1&sim=custom:<id>` (encodeURIComponent безопасен, lab декодирует param).
|
||||
- `frontend/js/labs/lab-glue.js` (+48/-1): `_bridgeCustomSimState(real)` — подключает custom-sim к
|
||||
тому же мосту `sim_state`/`apply_sim_state`, что и встроенные. getState=`{params,running}` /
|
||||
applyState=`setParam`+play/pause поверх SimEngine-инстанса (`real.instance()`). Регистрируется под
|
||||
ключом `_autoSim` (`custom:<dbid>`, т.к. apply у ученика берёт `_simStateRegistry[_autoSim]`),
|
||||
запускает `_startStateEmit`. Вызов в `_registerLazy.open()` после `real.open(ctx)` (только embed).
|
||||
- **Синхрон:** параметры слайдеров + play/pause — полный (demo-режим). Время `t` (фаза анимации)
|
||||
покадрово НЕ синхронится (by design; ученик крутит свой rAF при running). Аннотации/режим — через
|
||||
существующий конвейер без изменений (id-agnostic). Закрытие/смена: `frame.src='about:blank'` сносит
|
||||
весь документ iframe (SimEngine+rAF+слушатели) — утечек нет.
|
||||
- **Доступ:** двойная проверка — `simOpen` на сервере (постановка на доску) + `GET /custom-sims/:id`
|
||||
при загрузке спеки в iframe. Чужой draft → 403 на обоих. На доску только своё или published.
|
||||
- Верификация: `node --check` sim.js / lab-glue.js / извлечённого инлайна classroom.html — OK;
|
||||
эмодзи нет (UTF-8-скан текст-элементов: 0 в js, 11 в classroom.html — все ПРЕ-существующие
|
||||
×/⇒/реакции, не в моих строках); eval/new Function — 0 call-sites; `npm test` 240/248 pass
|
||||
(8 fail = тот же baseline: 3 auth.test + 5 page-тестов без jsdom; обе custom-sims-сьюты зелёные).
|
||||
git status: тронуты только мои 3 файла (+плановые .md); js/api.js НЕ нужен (методы есть с Ф3).
|
||||
- **Фаза 6 РЕАЛИЗОВАНА** (в рабочем дереве, не закоммичено — коммит за оркестратором). Файлы:
|
||||
`backend/src/controllers/customSimController.js` (+share/clone/related/addLink/removeLink, импорт
|
||||
`pushNotif`), `backend/src/routes/customSims.js` (+POST `/:id/share`, POST `/:id/clone`, GET
|
||||
@@ -135,9 +164,15 @@
|
||||
- Reuse > переписывание: сначала смотреть `_fx_motion`, `_graph_panel`, `graph.js`.
|
||||
|
||||
## RESUME STATE
|
||||
- Последний коммит фичи: — (Ф0..Ф6 реализованы, ещё не закоммичены — ждут оркестратора)
|
||||
- Текущая фаза: Phase 6 — Раздача / шаблоны / клон / программа (✅ Implemented, pending commit) →
|
||||
дальше Phase 7 — Доска онлайн-урока (последняя)
|
||||
- Последний коммит фичи: — (Ф0..Ф7 ВСЕ реализованы, ещё не закоммичены — ждут оркестратора)
|
||||
- Текущая фаза: Phase 7 — Доска онлайн-урока (✅ Implemented, pending commit) — ФИНАЛЬНАЯ.
|
||||
Дальше: Final Review (final-reviewer + security review) → коммит всех фаз → merge в master.
|
||||
- **Ф7 файлы (аддитивно, рабочее дерево по ним было чистым до правок):**
|
||||
`backend/src/controllers/classroom/sim.js` (simOpen принимает `custom:<dbid>` + access-check
|
||||
own|published|admin), `frontend/classroom.html` (пикер: свои+published custom через `_crLoadCustomSims`/
|
||||
`_crRenderSimGrid`; id `custom:<dbid>`), `frontend/js/labs/lab-glue.js` (`_bridgeCustomSimState` —
|
||||
мост sim_state/apply_sim_state для custom-sim поверх SimEngine; вызов в `_registerLazy.open`).
|
||||
js/api.js НЕ менялся. Синхрон: параметры+play/pause (не время t). Открытие — iframe `/lab?embed=1&sim=custom:<id>`.
|
||||
- Эндпоинты Ф6: share/clone/related/links на `/api/custom-sims/:id/*`; клиент `LS.customSimShare/
|
||||
Clone/Related/AddLink/DelLink`. Раздача = авто-publish + pushNotif (НЕ копия). Связи — lab_sim_links
|
||||
`sim_id='custom:<id>'`. Остаток Ф6: UI-редактор связей в билдере + чипы в каталоге (backend готов).
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
- [x] Phase 4: Билдер (редактор) [domain: frontend] → [subplan](./phase-4-builder-ui.md)
|
||||
- [x] Phase 5: Каталог (custom-sims в /lab) [domain: fullstack] → [subplan](./phase-5-catalog.md)
|
||||
- [x] Phase 6: Раздача / шаблоны / клон / программа [domain: fullstack] → [subplan](./phase-6-sharing.md)
|
||||
- [ ] Phase 7: Доска онлайн-урока [domain: fullstack] → [subplan](./phase-7-classroom.md)
|
||||
- [x] Phase 7: Доска онлайн-урока [domain: fullstack] → [subplan](./phase-7-classroom.md)
|
||||
|
||||
## Phase Progress Log
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
| Phase 4: Builder UI | frontend | ✅ Done | ✅ | ✅ | ✅ |
|
||||
| Phase 5: Catalog | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||
| Phase 6: Sharing | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||
| Phase 7: Classroom | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 7: Classroom | fullstack | ✅ Done | ✅ | ✅ | ✅ |
|
||||
|
||||
## Final Review
|
||||
- [ ] Comprehensive code review (final-reviewer)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 7: Доска онлайн-урока
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Status:** ✅ Implemented (pending commit)
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** fullstack
|
||||
|
||||
@@ -9,14 +9,21 @@
|
||||
sim, с синхроном параметров классу и аннотациями поверх.
|
||||
|
||||
## Tasks
|
||||
- [ ] Учитель в classroom выбирает sim для доски: добавить в список источников свои+published custom-sims (рядом со встроенными).
|
||||
- [ ] Открытие на доске через существующий `simOpen` (controller `classroom/sim.js`, роут `/:id/sim`) —
|
||||
для custom передаётся `custom:<id>`; рантайм `SimEngine` монтируется в sim-контейнер доски.
|
||||
- [ ] Синхрон состояния: параметры/play-pause/время транслировать классу через `simState/simMode`
|
||||
(как для встроенных) — ученики видят те же значения слайдеров и фазу анимации.
|
||||
- [ ] Аннотации поверх — через существующий `simAnnotate` (без изменений конвейера).
|
||||
- [ ] Закрытие/смена sim корректно размонтирует `SimEngine` (destroy).
|
||||
- [ ] Тест/проверка: открыть custom-sim на доске, подвигать слайдер у учителя → у ученика синхрон.
|
||||
- [x] Учитель в classroom выбирает sim для доски: добавить в список источников свои+published custom-sims (рядом со встроенными).
|
||||
→ `_crLoadCustomSims()` (`LS.customSimsList`) + мёрж в `_crRenderSimGrid`; карточка с бейджем «Моя», id=`custom:<dbid>`.
|
||||
- [x] Открытие на доске через существующий `simOpen` (controller `classroom/sim.js`, роут `/:id/sim`) —
|
||||
для custom передаётся `custom:<id>`. Рантайм НЕ монтируется напрямую в доску: доска уже грузит sim
|
||||
в **iframe** `/lab?embed=1&sim=...`; для custom это `/lab?embed=1&sim=custom:<id>`, где `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)
|
||||
@@ -34,10 +41,41 @@ sim, с синхроном параметров классу и аннотаци
|
||||
- classroom.html большой (8240 строк) — править точечно.
|
||||
|
||||
## Review Checklist
|
||||
- [ ] Все задачи выполнены
|
||||
- [ ] Синхрон параметров учитель→ученики работает
|
||||
- [ ] Доступ к custom-sim на доске проверяется
|
||||
- [ ] Встроенные sim на доске не сломаны; SimEngine корректно размонтируется
|
||||
- [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:<dbid>`,
|
||||
валидирует доступ (владелец ИЛИ 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:<dbid>`,
|
||||
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:<dbid>`), запускает `_startStateEmit`. Тем же `_simStateRegistry`/каналом, что встроенные.
|
||||
|
||||
**Степень синхрона:** параметры слайдеров + признак воспроизведения (play/pause) — ПОЛНЫЙ синхрон
|
||||
учитель→ученики (demo-режим). Время `t` (фаза анимации) НЕ синхронизируется покадрово: ученик
|
||||
запускает свой rAF при running=true, фаза может разъезжаться. Достаточно по требованиям фазы.
|
||||
При желании в будущем: добавить `t` в state и `inst._t` сеттер (потребует расширения SimEngine API).
|
||||
|
||||
**Открытие на доске:** через iframe `/lab?embed=1&sim=custom:<id>` (НЕ прямой 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.
|
||||
|
||||
Reference in New Issue
Block a user