# Phase 5: Каталог (custom-sims в /lab) **Status:** ✅ Implemented (в рабочем дереве, не закоммичено — коммит за оркестратором) **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** fullstack ## Objective Сохранённые custom-симуляции появляются и играют в /lab наравне со встроенными; раздел «Мои симуляции», редактирование/удаление из каталога, deep-link. ## Tasks - [x] При загрузке /lab подтягивать custom-sims (свои + published) из `GET /api/custom-sims` (`LS.customSimsList()`) и регистрировать ЛЕНИВЫЕ манифесты в LabRegistry. spec НЕ грузится на старте — тянется при первом открытии (`LS.customSimGet` → `registerSpecSim` Ф0-адаптера). - [x] Карточки в каталоге: категория/предмет(grade)/из меты; бейдж «Моя» (owner) / «Опубликована» (status) / «Черновик» (own draft). - [x] Раздел «Мои симуляции» в /lab — отдельная секция `#custom-sim-section` в `#lab-home` (создаётся динамически, без правок lab.html/CSS); уважает текущий фильтр категорий. - [x] Кнопки на карточке custom-sim: «Редактировать» → `/sim-builder?id=`, «Удалить» (`LS.customSimDelete`) — только владельцу (`owner_id === user.id`). - [x] Deep-link `/lab?sim=custom:` открывает напрямую: хук `LabCustom.resolveId` в `openSim` (lab-init.js) переводит `custom:` → реестровый id `customsim_`; для custom deep-link открытие отложено до загрузки списка (и в обычном, и в embed-режиме). - [x] Ленивая загрузка: движок (`_sim_expr/_sim_engine/_sim_adapter`) уже eager в lab.html (Ф0), поэтому отдельный ленивый файл не нужен; лениво грузится только spec (тяжёлый JSON) при открытии. `_sim_deps.js` НЕ тронут. ## Files to Modify/Create - `frontend/js/labs/lab-glue.js` и/или `lab-init.js` — загрузка+регистрация custom-sims, карточки, фильтр (modify) - `frontend/js/labs/_sim_deps.js` — `_sim_*.js` в ленивые зависимости (modify) - `js/api.js` — при необходимости (modify, опц.) ## Acceptance Criteria - Сохранённая в Ф4 симуляция видна в /lab, открывается и играет. - «Мои симуляции» показывает свои (вкл. draft); published видят и другие. - Edit/Delete с карточки работают; deep-link открывает. - Старт /lab не тормозит (движок грузится лениво). ## Notes - НЕ ломать существующий каталог встроенных (lab_sims) — custom-список добавляется поверх. - id-неймспейс `custom:` чтобы не конфликтовать со встроенными. ## Review Checklist - [x] Все задачи выполнены - [x] Встроенные симуляции и старт /lab не регрессировали (custom исключены из основной сетки по флагу `_custom`; падение загрузки списка не ломает каталог — try/catch + мягкий warn) - [x] Draft видит только владелец; published — все (видимость обеспечивает сервер Ф3: `customSimsList` отдаёт свои любого статуса + чужие published; бейджи/кнопки — по `owner_id`) - [x] Ленивая загрузка spec работает (кэш + дедуп; на старте спеки не грузятся) ## Handoff to Next Phase ### Что реализовано (Phase 5) Только аддитивные правки двух файлов параллельной сессии — без рефактора их кода: - **`frontend/js/labs/lab-init.js`** (+7 строк): в начало `openSim(id)` добавлен хук — `if (window.LabCustom && LabCustom.resolveId) id = LabCustom.resolveId(id) || id;` Переводит `custom:` → реестровый id `customsim_` (LabRegistry.get/has обрезают часть после `:`, поэтому в реестре двоеточие недопустимо). Для встроенных id — no-op. - **`frontend/js/labs/lab-glue.js`**: - `renderSims()`: 1 строка в merge — `&& !m._custom` (custom-манифесты не попадают в основную сетку встроенных) + вызов `LabCustom.renderSection(_catFilter)` после рендера грида. - init-блок (non-embed): после `renderSims()` зовёт `LabCustom.init()`; для `?sim=custom:*` открытие отложено до резолва списка. Аналогично в embed-ветке (на `load`). - **`window.LabCustom`** (новый IIFE в конце файла): `init()` (fetch списка, регистрация ленивых манифестов, рендер секции), `resolveId`, `renderSection`, `ensureSpec`, `del`. ### Как custom-sims попадают в каталог и открываются 1. `LS.customSimsList()` → мета (без spec). Для каждой — `_registerLazy(meta)`: в LabRegistry кладётся манифест-заглушка `id='customsim_'`, `_custom:true`, мета-поля, и `open()`, который при первом вызове лениво тянет spec. 2. Секция «Мои симуляции» (`#custom-sim-section`) рендерится из `_meta` (НЕ из реестра): карточки `.sim-card` с `data-open="custom:"`, бейджами и (владельцу) кнопками edit/del. Делегат кликов на секции: открыть / `/sim-builder?id=` / `LS.customSimDelete`. 3. Открытие: `openSim('custom:')` → `resolveId` → `customsim_` → дисп. реестра → `open()` заглушки → `ensureSpec(dbid)` (`LS.customSimGet`, кэш+дедуп) → `spec.id=` → `registerSpecSim(spec)` (Ф0-адаптер строит реальный SimEngine-манифест, ЗАМЕНЯЕТ заглушку на месте) → `setActive(real)` + `real.open(ctx)` (монтирует SimEngine). Повторное открытие — синхронно, реальный манифест уже в реестре, spec из кэша. ### id-неймспейс - Deep-link / клик / `data-open`: **`custom:`**. - LabRegistry / host: **`customsim_`** (без `:`). Конвертация — только в `resolveId`. ### Формат карточки preview-SVG (плейсхолдер) + cat-бейдж (`.sim-cat`) + бейджи «Моя»/«Опубликована»/«Черновик» + title + desc (+ «N класс») + (владельцу) ряд кнопок «Редактировать»/«Удалить» (inline SVG `.ic`). ### Риски / заметки для Ф6 - `_loadRelated('customsim_')` дергает `/api/lab/sims/.../related` (404 для custom) — тихо глотается. Если Ф6 заведёт связи custom-sim с программой — учесть этот id. - Удаление: после `customSimDelete` карточка убирается из секции, но манифест-заглушка остаётся в LabRegistry (LabRegistry не имеет unregister). Не критично (карточки нет, deep-link на удалённую вернёт 404 при ensureSpec). Если Ф6/Ф7 потребуют чистку — добавить unregister в реестр. - Ф6 (раздача/публикация/клон/шаблоны): кнопку «Поделиться/Раздать классу» добавлять в `_cardHtml` (ещё один `data-act`); публикацию toggle — там же. Клон — новый `LS.customSimCreate` со spec из `ensureSpec`. Источник spec для доски (Ф7) — `LabCustom.ensureSpec(dbid)` или живой `LabRegistry.get('customsim_').instance()`.