chore(plan): lab-split 5-phase plan

PLAN.md + 5 subplans + CONTEXT.md

Strategy: Incremental | Mode: Automated | Execution: Direct

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-22 22:33:41 +03:00
parent 58cff2285e
commit 77ebe9e3e4
7 changed files with 429 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Feature Context: Lab.html Split
## Current State
- lab.html: 5180L total
- Lines 10-866: inline `<style>` block (856L)
- Lines 868-4303: HTML body with 39 sim-panels
- Lines 4304-4326: external script tags (engine modules)
- Lines 4327-5152: inline `<script>` block (825L glue)
- Lines 5153+: more script tags
- 39 engine modules already extracted in `frontend/js/labs/*.js`
- `lab-init.js` (543L) is the orchestrator
- 265 hardcoded brand colors throughout
- 1017 inline `style=` attributes
## Key Discoveries
- Each sim has a `<div id="sim-X" class="sim-proj-wrap" style="display:none">` pattern
- Sim activation likely via `sim-switcher` element + JS that toggles display
- Lab has its own large CSS scope that doesn't conflict with ls.css (verify)
## Cross-Phase Dependencies
- **Phase 1** (extract style) — independent
- **Phase 2** (extract glue) — independent of Phase 1
- **Phase 3** (token purification) — can run after Phase 1 (CSS file becomes purification target)
- **Phase 4** (hash-router) — needs Phase 2 (router code in lab-glue.js easier to extend than inline)
- **Phase 5** (template lazy) — needs Phase 4 (router triggers template activation)
## Temporary Workarounds
(пусто — заполняется implementer'ом)
## Implementation Notes
### Что НЕ трогаем
- `frontend/js/labs/*.js` engine-классы (CollisionSim, ProjectileSim и т.д.) — они работают
- `frontend/js/labs/lab-init.js` — orchestrator, может расширяться, но не переписываться целиком
- `<canvas>` элементы и их id — engine-классы binds к ним по id
### Безопасные паттерны (из прошлой работы)
- Extract `<style>` → external file: добавить `<link rel="stylesheet">` в head, скопировать содержимое в .css, удалить inline. Проверить визуал curl-200 + spot-check styles.
- Extract `<script>` → external file: добавить `<script src=>` ПОСЛЕ engine-modules, переместить glue. Watch for global-leak (если inline relies on top-level vars).
- Token purification: replace `#9B5DE5``var(--violet)`, keep tints (`rgba(...)`), keep curated palettes.
+75
View File
@@ -0,0 +1,75 @@
# Feature: Lab.html Split
**Branch:** `feature/lab-split`
**Base branch:** `master`
**Created:** 2026-05-22
**Status:** 🟡 In Progress
**Strategy:** Incremental
**Mode:** Automated
**Execution:** Direct
## Summary
Расщепить `frontend/lab.html` (5180L монолит UI-shell) на модульную структуру: extract inline CSS + inline JS-glue, token purification (265 hardcodes → vars), hash-router для deep-links, optional `<template>` lazy-mount.
**Discovery:** симуляции уже extracted в `frontend/js/labs/*.js` (39 engine-классов). lab.html — это HTML shell с 856L inline CSS + 3435L DOM + 825L inline glue-JS.
**Цели:**
- lab.html structure clearer (CSS / JS вынесены, HTML только DOM)
- Хардкодов ≤ 30 (от 265)
- Deep-link `#sim/projectile` работает
- Все 39 симуляций без регрессий
- Pre-commit hook проходит на каждой фазе
## Build & Test Commands
- **Start:** `cd backend && npm start` (vanilla JS — нет билда)
- **Test:** `cd backend && npm test` (66 tests / 63 pass / 3 baseline-fail)
- **Lint:** `cd backend && npm run lint:routes`
- **Smoke:** `curl -sI http://localhost:3000/lab` → 200; манульно открыть несколько sim
- **Pre-commit hook активен** — runs all of the above automatically
## Phases
- [ ] Phase 1: Extract inline `<style>``frontend/css/lab.css` [domain: frontend] → [subplan](./phase-1-extract-style.md)
- [ ] Phase 2: Extract inline glue `<script>``frontend/js/labs/lab-glue.js` [domain: frontend] → [subplan](./phase-2-extract-glue.md)
- [ ] Phase 3: Token purification (265 hardcodes) [domain: frontend] → [subplan](./phase-3-token-purify.md) (parallelizable with 1 или 2)
- [ ] Phase 4: Hash-router for sim deep-links [domain: frontend] → [subplan](./phase-4-hash-router.md)
- [ ] Phase 5: `<template>` lazy-mount (stretch) [domain: frontend] → [subplan](./phase-5-template-lazy.md)
## Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|-------|--------|--------|--------|-------|-----------|
| Phase 1: Extract style | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 2: Extract glue | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 3: Token purify | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 4: Hash-router | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 5: Template lazy | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
## Final Review
- [ ] Comprehensive code review
- [ ] All 39 simulations smoke-tested
- [ ] No console errors on /lab
- [ ] pre-commit hook passes
- [ ] Merged to `master`
## Acceptance Criteria (whole feature)
- lab.html без inline `<style>` блока (856L moved out)
- lab.html без inline glue `<script>` блока (825L moved out)
- Хардкодов brand colors ≤ 30 (curated palettes сохраняются)
- `#sim/projectile`, `#sim/newton` и др. — deep-link работают, F5 восстанавливает
- 39 симуляций функциональны (canvas рендерится, кнопки работают)
- Pre-commit hook чистый на каждом коммите
## Tech Stack & Conventions Reference
- vanilla JS, no bundler, Express static serve
- `window.LS.*` namespace (api.js)
- `LS.modal`, `LS.confirm`, `LS.toast`
- ls.css design tokens (--violet/--cyan/--green/--pink/--amber + --text/--text-2/--text-3 + spacing + radii)
- Lucide icons (CDN) + inline SVG `.ic`
- **No emoji в коде** (pre-commit блокирует)
- **No grep tool**, только ast-index для search
- Existing labs/*.js engine-классы НЕ трогаем — они уже extracted
+49
View File
@@ -0,0 +1,49 @@
# Phase 1: Extract inline `<style>` → `frontend/css/lab.css`
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Вынести 856L inline `<style>` блока из lab.html в отдельный `frontend/css/lab.css`. После — lab.html становится короче на ~856L, CSS можно править/рефакторить независимо.
## Tasks
- [ ] Создать `frontend/css/lab.css`
- [ ] Скопировать содержимое `<style>...</style>` из lab.html (lines 10-866) в lab.css
- [ ] Удалить inline `<style>` блок из lab.html
- [ ] Добавить `<link rel="stylesheet" href="/css/lab.css">` в `<head>` (после `/css/ls.css`)
- [ ] Verify: `curl -sI http://localhost:3000/lab` → 200
- [ ] Spot-check: открыть в браузере, sim-toolbar/panels выглядят как раньше
- [ ] Pre-commit hook passes
## Files to Modify/Create
- `frontend/css/lab.css` — NEW (~856L)
- `frontend/lab.html` — удалить `<style>` блок, добавить `<link>` (net 855L)
## Acceptance Criteria
- lab.html без `<style>` блока (только `<link>` к /css/lab.css)
- `/lab` отвечает 200
- Визуально lab выглядит идентично pre-Phase-1
- Pre-commit hook чистый
## Notes
- Если в CSS есть `@import` / `url(...)` paths — проверить что они всё ещё валидны от нового origin (/css/lab.css base)
- CSP в server.js: разрешает `'self'` для styles, нет проблем
- Watch for: CSS-variables defined inline могут оказаться нужны другим inline blocks → проверить нет ли таких dependencies
## Review Checklist
- [ ] lab.css не пустой и содержит весь CSS из inline блока
- [ ] lab.html не содержит `<style>` блок (только `<link>`)
- [ ] No emoji в коде (pre-commit проверит)
- [ ] Server возвращает 200
- [ ] Spot-check: открыть /lab, sim-grid и sim-toolbar отображаются нормально
## Handoff to Next Phase
<!-- Implementer заполнит: какой именно баг был при extract (если был), какие inline-style overrides остались (Phase 3 будет с ними работать). -->
+68
View File
@@ -0,0 +1,68 @@
# Phase 2: Extract inline `<script>` glue → `frontend/js/labs/lab-glue.js`
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Вынести 825L inline `<script>` блока (lines 4327-5152) из lab.html в отдельный `frontend/js/labs/lab-glue.js`. Этот блок содержит glue-код (sim-switcher logic, init helpers, event wiring). lab-init.js остаётся orchestrator'ом.
## Tasks
- [ ] Определить точные границы inline `<script>` блока (line 4327 start, find matching `</script>`)
- [ ] Создать `frontend/js/labs/lab-glue.js`
- [ ] Скопировать содержимое в lab-glue.js
- [ ] **Сохранить порядок загрузки**: lab-glue.js должен подгружаться ПОСЛЕ всех `labs/*.js` engine-модулей И ПЕРЕД `labs/lab-init.js` (или после — зависит от dependencies, проверить!)
- [ ] Удалить inline блок из lab.html
- [ ] Добавить `<script src="/js/labs/lab-glue.js"></script>` в правильное место
- [ ] Verify: page loads, `console.log` ошибок нет, sim-switcher работает
- [ ] Smoke: переключить 3-4 разных sim, проверить что render запускается
## Files to Modify/Create
- `frontend/js/labs/lab-glue.js` — NEW (~825L)
- `frontend/lab.html` — удалить inline `<script>` блок, добавить `<script src>` тег (net 823L)
## Acceptance Criteria
- lab.html без большого inline `<script>` блока на lines 4327-5152
- `/lab` отвечает 200
- No `ReferenceError` / `is not defined` в console (load-order правильный)
- Sim-switcher переключает sims корректно
- 5 любых симуляций инициализируются и рендерятся
## Notes
### Load-order анализ
Перед extract — проверить какие globals использует inline glue:
- Если использует `CollisionSim` (из engine-modules) → нужно загружаться ПОСЛЕ engine-modules
- Если использует `Lucide` (CDN) → после Lucide
- Если других inline-vars нет — безопасно вынести
### Watch for
- Inline `<script>` без `defer` атрибута выполняется sync — после переноса в external может выполниться раньше DOM ready. Возможно нужен `DOMContentLoaded` wrapper, либо `defer` атрибут.
- `window.xxx = ...` глобальные exports должны остаться (onclick handlers HTML on них опираются)
### Strategy
1. Read весь inline блок
2. Identify все function/var declarations
3. Скопировать как есть в lab-glue.js
4. Add at top: `'use strict';` если ещё нет
5. Тестировать пристально
## Review Checklist
- [ ] lab-glue.js загружается в правильном порядке (после engine modules)
- [ ] No console errors на /lab
- [ ] Sim-switcher работает (тест: переключить projectile → newton → chemsandbox)
- [ ] Все onclick handlers HTML работают
- [ ] No emoji в коде
- [ ] Pre-commit hook passes
## Handoff to Next Phase
<!-- Implementer фиксирует: где находится hash-router-точка-расширения (Phase 4 будет добавлять hashchange handler сюда). -->
+62
View File
@@ -0,0 +1,62 @@
# Phase 3: Token purification — 265 hardcoded colors → vars
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Заменить 265 хардкодных brand-цветов в lab-related files на `var(--token)` где это семантически корректно. Сохранить curated palettes (subject-specific colors) и canvas-fillStyle (CSS vars не resolve'ятся в canvas context).
## Tasks
- [ ] Идентифицировать все хардкоды в:
- `frontend/lab.html` (HTML body + remaining inline styles)
- `frontend/css/lab.css` (после Phase 1)
- `frontend/js/labs/lab-glue.js` (после Phase 2)
- [ ] Заменить direct token matches:
- `#9B5DE5``var(--violet)`
- `#06D6E0``var(--cyan)`
- `#06D664``var(--green)` (also `#06D6A0` если есть)
- `#F15BB5``var(--pink)`
- `#FFB347``var(--amber)`
- `#0F172A``var(--text)`
- `#3D4F6B``var(--text-2)`
- `#56687A``var(--text-3)`
- `#EEF2FF``var(--bg)`
- [ ] KEEP (НЕ менять):
- Tinted/alpha (`rgba(155,93,229,0.12)` etc.) — CSS не имеет color-mix() без deps
- Canvas `ctx.fillStyle = "#..."` — CSS vars не работают в canvas
- Curated subject palettes (bio violet / chem green / math cyan / phys amber) если они вложены массивом
- Slightly-different shades (`#9B5DE6``#9B5DE5`) — это намеренно другой цвет
- [ ] Semantic aliases: использовать `var(--danger)` / `var(--success)` / `var(--warning)` / `var(--info)` в semantic context
## Files to Modify
- `frontend/lab.html` — заменить hardcodes
- `frontend/css/lab.css` (после Phase 1) — заменить hardcodes
- `frontend/js/labs/lab-glue.js` (после Phase 2) — заменить hardcodes если есть
## Acceptance Criteria
- Хардкодов brand-colors ≤ 30 (от 265, target 90%+ replacement)
- Визуально lab выглядит идентично
- Pre-commit hook passes
- No regression в любой sim
## Notes
- Lab.html сейчас содержит много inline `style="color: #XXX"` — multi-line replacement через ast-index search + manual replacement
- Don't over-aggressive — если color используется один раз в curated palette decoration, лучше keep
- Document log: "Заменено: N, оставлено: M (reasons listed)"
## Review Checklist
- [ ] Counts reported: before / after
- [ ] Spot-check 3 sims визуально (canvas рендеринг не поменялся)
- [ ] No emoji в коде
- [ ] Pre-commit hook passes
## Handoff to Next Phase
<!-- Implementer: какие места были tricky, есть ли паттерны которые нужно унифицировать в будущем. -->
+70
View File
@@ -0,0 +1,70 @@
# Phase 4: Hash-router для sim deep-links
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Сделать `#sim/projectile`, `#sim/newton`, `#sim/chemsandbox` etc. → открывают конкретный sim в lab. F5 на любом deep-link восстанавливает sim. Browser back/forward переключают между симуляциями. По образцу admin-redesign Phase 1 router.
## Tasks
- [ ] В `frontend/js/labs/lab-glue.js` (или новый `frontend/js/labs/lab-router.js`):
- На load: прочитать `location.hash`, parsr `#sim/<name>`, активировать соответствующий sim
- Listen `hashchange`: при изменении hash → переключить sim
- При программном переключении sim (через sim-switcher UI) → обновить hash без recursion (флаг `_routerNavigating`)
- [ ] Map sim-name → sim-id:
- `#sim/projectile` → activate `<div id="sim-proj">`
- `#sim/newton` → activate `<div id="sim-dynamics">` (или какой-там)
- Полный mapping из существующих sim-ID
- [ ] Fallback: unknown hash → ignore (показать default sim)
- [ ] Verify: F5 на `/lab#sim/projectile` восстанавливает projectile sim
## Files to Modify
- `frontend/js/labs/lab-glue.js` — добавить router code (~50-100L)
- `frontend/lab.html` — без изменений (или + 1 script tag если делаем отдельный lab-router.js)
## Acceptance Criteria
- F5 на `/lab#sim/X` восстанавливает соответствующий sim
- Browser back/forward переключают между sims
- Click на sim-switcher обновляет URL (`#sim/X` в адресной строке)
- Unknown hash (`#sim/nonexistent`) → console.warn + fallback на default
- 5 deep-link проверены вручную (projectile, newton, chemsandbox, gas, mirror)
## Notes
### Recursion guard pattern (из admin-redesign Phase 1)
```js
let _navigating = false;
function navigateTo(simId) {
_navigating = true;
location.hash = '#sim/' + simId;
setTimeout(() => { _navigating = false; }, 0);
activateSim(simId);
}
window.addEventListener('hashchange', () => {
if (_navigating) return;
const m = location.hash.match(/^#sim\/([\w-]+)/);
if (m) activateSim(m[1]);
});
```
### Reference
`frontend/js/admin/router.js` (admin-redesign Phase 1) — read for inspiration. Adapt to lab context.
## Review Checklist
- [ ] Hash deep-link работает для 5 проверенных симов
- [ ] Browser back/forward работают
- [ ] No console errors
- [ ] No infinite-loop при программной активации
- [ ] Pre-commit hook passes
## Handoff to Next Phase
<!-- Implementer: full mapping #sim/X → sim-ID для Phase 5 (template lazy). -->
+59
View File
@@ -0,0 +1,59 @@
# Phase 5: `<template>` lazy-mount (stretch goal)
**Status:** ⬜ Not Started
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
## Objective
Уменьшить initial DOM size: вместо 39 `<div id="sim-X">` с `display:none` — обернуть каждую в `<template id="tpl-sim-X">`, активация sim → clone template + mount into placeholder. Initial page load становится легче (нет render'а скрытых canvas-ов).
**ВНИМАНИЕ:** Это stretch goal — может быть пропущен если Phase 1-4 успешны и не хочется добавлять риск.
## Tasks
- [ ] Identify все `<div id="sim-X" class="sim-proj-wrap" style="display:none">` блоки (~38 штук, скрытых; 1 default visible)
- [ ] Обернуть каждый в `<template id="tpl-sim-X">...</template>`
- [ ] Создать mount point `<div id="sim-mount"></div>` (или использовать existing #sim-grid)
- [ ] В lab-glue.js / lab-router.js:
- `activateSim(name)` → clone `<template id="tpl-sim-X">` → replace content of `#sim-mount`
- Initialize sim engine (CollisionSim, ProjectileSim, etc.) **после** mount'а
- Cleanup previous sim engine (stop animations, remove listeners) перед switching
- [ ] Test: переключить 5 sims подряд, no memory leak, animations stop когда sim de-activated
## Files to Modify
- `frontend/lab.html` — wrap 38+ `<div id="sim-X">` в `<template>` (большое change but mechanical)
- `frontend/js/labs/lab-glue.js` — add template-clone activation logic
## Acceptance Criteria
- Initial DOM size уменьшен (verify через DevTools — count nodes)
- Все 39 sims активируются и работают
- No memory leak при переключении (verify через DevTools Performance / Memory tab — heap не растёт неограниченно)
- Sim engines properly cleanup previous instance
## Risks (HIGH)
- **Sim engines binds to specific canvas IDs by JS** (e.g. `document.getElementById('canvas-proj')`). После template clone — `getElementById` может вернуть element до момента когда engine ищет его. Time it carefully.
- **Animations не stop'аются** автоматически при template removal. Engine классы должны иметь `destroy()` метод — проверить.
- **Initial sim** (тот что default visible) — изначально mount'ed, не template. Обработать особо.
## Notes
Если в течение фазы выясняется что engine классы плохо cleanup'ятся (нет `destroy()`, requestAnimationFrame продолжает работать), MARK phase as 🟡 partial и оставить как known limitation. Не нужно рефакторить 30 engine классов.
## Mitigation: opt-out
Если эта фаза создаёт реальный риск — **пропустить**. lab.html без Phase 5 будет ~4000L (после Phase 1 убрали 856L style + 825L glue), что уже сильно лучше 5180L. Phase 5 опционален.
## Review Checklist
- [ ] All 39 sims активируются успешно
- [ ] No memory leak при переключении (Heap snapshot до и после — diff не растёт)
- [ ] Browser DevTools showsменьше nodes на initial load
- [ ] No console errors
## Handoff to Next Phase
<!-- Финальная фаза. Implementer записывает: что осталось как known-limitation, какие engine классы не имеют destroy(). -->