# ТЗ #337 — ленивый editor-runtime и multi-asset frontend - Issue: https://github.com/Matysh/houseplan-card/issues/337 - Тип: техдолг / производительность - Приоритет: P2 - Трек: полный — задача влияет на производительность и меняет несколько поверхностей и release-asset contract, поэтому не проходит критерии `small` из `PROCESS.md` §5 - Связанные задачи: #34 — направление декомпозиции frontend; #62 — единый eager-реестр двух текущих языков, lazy i18n в этой задаче отсутствует ## 1. Сценарий Основная персона — жилец или гость (`docs/SCOPE.md`), который открывает дашборд Home Assistant на телефоне, планшете или настенной панели, чтобы быстро увидеть дом и управлять устройствами в View. Он не должен загружать код трёх администраторских редакторов, пока сам не попросил открыть редактор. Вторичная персона — владелец/администратор. При первом входе в Plan, Devices или Background после открытия дашборда он допускает короткое ожидание загрузки, но не потерю View, текущего пространства, масштаба или незавершённого клика. ## 2. Что человек увидит до и после До изменения каждый вход на дашборд загружает весь редактор; после изменения обычный View становится рабочим после заметно меньшей загрузки, а код редактора подгружается только перед первым фактическим входом в него, без изменения интерфейса и поведения редакторов. При редкой ошибке загрузки редактора View остаётся рабочим, а пользователь видит локализованное сообщение с просьбой обновить страницу вместо зависшей или наполовину открытой панели. ## 3. Подтверждённое исходное состояние Замеры выполнены на `origin/dev` `eff786f`, версия 1.68.1: - `dist/houseplan-card.js` — 1 353 147 B raw / 378 335 B gzip; - `src/houseplan-card.ts` даёт около 1 052 KiB кода до terser и содержит View, editor state machines, команды, панели и диалоги в одном классе; - backend раздаёт только точный файл `/houseplan_files/houseplan-card.js`; относительный chunk сейчас получит 404; - bundle sync, freshness, demo, CI и release scripts считают артефактом один JS-файл; - консервативная минификация всех текущих статических `css\`\``-литералов уменьшает bundle примерно на 32 016 B raw, но только на 2 114 B gzip. Она не заменяет реальное code splitting; - #62 намеренно сохранила `en` и `ru` синхронными/eager. Утверждение старого описания #337 о lazy dictionaries неверно и не является предпосылкой ТЗ. ## 4. Цели 1. Не загружать runtime Plan/Devices/Background и editor-only dialogs в первом рабочем View. 2. Ограничить сумму gzip initial View graph величиной **250 KiB**. 3. Сохранить наблюдаемое поведение View, переходов, редакторов, конфигурации и Home Assistant card editor. 4. Сделать несколько frontend-файлов полноценным проверяемым артефактом HACS, backend, demo, CI и release automation. 5. Безопасно минифицировать статические Lit CSS templates без визуальной разницы. ## 5. Не-цели - не менять stored config/layout и не добавлять миграцию; - не менять UX, состав инструментов, DOM-контракт, pointer thresholds, Undo/Redo, touch policy или геометрию; - не заменять и не упрощать `polyclip-ts`; - не делать lazy loading текущих i18n dictionaries; - не оптимизировать backend API или SVG/Glow вычисления, если они нужны View; - не считать успехом маленький entry, который тут же синхронно или автоматически скачивает тот же код до готовности View; - не делать автоматический idle-prefetch editor runtime: он вернул бы сетевой расход всем пользователям, которые редактор не открывают. ## 6. Пользовательский контракт ### 6.1 Первый View 1. `houseplan-card` и `houseplan-space-card` регистрируются так же надёжно, как до изменения; HA не показывает промежуточное `Custom element doesn't exist`. 2. View считается готовым только когда видны план и устройства и доступны обычные View-действия. Все JS-файлы, обязательные до этого момента, входят в initial View graph и budget §13. 3. Ни один editor-only asset не запрашивается до намерения пользователя открыть редактор или editor-only диалог. Hover без клика и простои страницы не считаются намерением. 4. Пустая новая установка сохраняет текущий onboarding: диалог создания первого пространства не может зависеть от editor runtime. ### 6.2 Вход в редактор 1. Нажатие Plan, Devices или Background сначала запускает единственный shared loader. Mode, editor chrome и editor camera не коммитятся до успешной установки runtime. 2. Повторные/конкурентные клики используют один Promise; runtime не скачивается и не устанавливается дважды. 3. Пока загрузка занимает меньше 150 ms, отдельная плашка не появляется. После 150 ms поверх неизменившегося View показывается неблокирующий существующий transition surface с текстом «Загружаем редактор…» / “Loading editor…”. 4. После успеха выполняется обычный переход текущего `mode-transition`; первая editor-панель не появляется в полуготовом состоянии. Пространство, View zoom snapshot и selection contracts сохраняются. 5. После первой успешной загрузки все три редактора и editor-only dialogs используют уже установленный runtime без новой сети. 6. Lovelace GUI editor загружается тем же lazy graph через async `getConfigElement()`. Создание config editor не должно заставлять View заранее загружать editor runtime. ### 6.3 Editor-only dialogs из View К editor-only относятся формы, изменяющие plan/device/decor data и не нужные для обычного просмотра. Если такая форма вызывается из доступной в View цепочки (например, Edit из device info), loader завершается до открытия формы. Info, more-info, подтверждение действия, статусы проёмов, kiosk controls и onboarding остаются eager, если они нужны View сами по себе. Точный список экспортов фиксирует `editor-runtime-manifest.ts`; новый editor-only диалог должен добавляться туда, а не импортироваться из View entry напрямую. ### 6.4 Ошибка загрузки и обновление во время открытой вкладки 1. Первый сетевой/parse/fingerprint failure повторяет import один раз с versioned cache-buster текущей карточки. До retry View остаётся рабочим. 2. После повторной ошибки mode остаётся `view`, editor session не создаётся, toolbar/camera/selection не меняются. Показывается локализованное сообщение: «Не удалось загрузить редактор. Обновите страницу и повторите попытку.» / “Could not load the editor. Refresh the page and try again.” 3. Runtime сообщает build fingerprint до установки. Несовпадение entry/runtime рассматривается как тот же failure; код разных версий не смешивается. 4. Автоматический hard reload запрещён: он может прервать другое действие на HA-дашборде. Пользователь сам решает, когда обновить страницу. 5. Ошибка editor asset никогда не скрывает план и не ломает View-действия. ## 7. Архитектурный контракт frontend ### 7.1 Граница runtime Создаётся `src/editors/runtime/` с двумя явными сторонами: - eager `editor-loader.ts`: маленькая state machine `idle → loading → ready | failed`, dedupe, retry и fingerprint handshake; - lazy `houseplan-editor-runtime.ts`: composition root редакторов; - `editor-host-port.ts`: typed минимальный порт к snapshot, командами, save, translation, requestUpdate и mode transition владельца карточки; - editor-specific render/controller modules, вынесенные из `houseplan-card.ts` законченными ответственностями. Runtime не получает `HouseplanCard` через `any`, не патчит prototype и не читает произвольные private fields. Он работает через typed host port и собственный state. Число `any` в `src/` не увеличивается. Pure geometry authorities остаются в текущих модулях и передаются runtime как обычные imports; второй модели геометрии не создаётся. Изменение является крупным independently releasable slice направления #34: root сохраняет HA lifecycle, View projection/render, server revisions, common dialogs, navigation и visual continuity; runtime получает editor gestures, tool state, editor chrome/secondary tray, editor-only drafts/dialogs и команды. ### 7.2 Import graph - eager entry не имеет static import из `src/editors/runtime/houseplan-editor-runtime.ts` или editor-only descendants; - единственное runtime-ребро — `import()` внутри loader; - общие модули, реально нужные View и editor, остаются eager либо становятся shared chunk, который входит в initial budget; - модуль не признаётся editor-only только по имени: Rollup manifest и тест import graph являются источником истины; - `polyclip-ts` сохраняется там, куда его помещают реальные потребители. ## 8. Multi-asset build и раздача ### 8.1 Выход Rollup Rollup пишет дерево: ```text dist/ houseplan-card.js houseplan-assets/ .js houseplan-assets.json ``` Имена chunk содержат content hash. Manifest детерминированно содержит: - source fingerprint; - entry filename; - для каждого asset: relative path, SHA-256, raw bytes, gzip bytes; - static imports и dynamic imports; - рассчитанные `initialViewGzipBytes` и `lazyEditorGzipBytes`. Порядок записей и gzip settings фиксированы, поэтому Windows/Linux build одного SHA создаёт побайтово одинаковые committed snapshots. ### 8.2 Backend route Точный публичный URL `/houseplan_files/houseplan-card.js` остаётся без изменений. Chunks раздаются публичным read-only view по `/houseplan_files/houseplan-assets/{filename}`. View: - принимает только один basename без `/`, `\\`, `..` и percent-decoded обходов; - раздаёт только `.js`, присутствующий в текущем `houseplan-assets.json`; - разрешает real path только внутри `frontend/houseplan-assets/`; - возвращает 404 для отсутствующего/неразрешённого asset; - не требует auth, потому что Lovelace ESM assets должны загружаться до карточки; - перечитывает/инвалидирует manifest после обновления integration, чтобы config entry reload не оставлял старый allowlist. Plans и marker files не возвращаются в public static path; существующий signed content API не меняется. ### 8.3 Копии и HACS `bundle:sync` атомарно синхронизирует **всё дерево**, а не один файл: 1. `dist/` — build output; 2. `custom_components/houseplan/frontend/` — committed release snapshot; 3. `demo/srv/assets/` — materialized untracked test copy. Перед копированием target asset directory очищается только по списку старого manifest, с path containment check; посторонние файлы не удаляются. После копии каждый hash сверяется с manifest. `houseplan.zip` включает entry, manifest и все chunks. Отдельный GitHub asset `houseplan-card.js` сохраняется как диагностический entry-файл, но каноническая установка остаётся целым `houseplan.zip`; документация не обещает, что один скачанный entry без integration assets является самостоятельной установкой. Release verification проверяет полный manifest внутри zip. ## 9. CSS minification Build plugin обрабатывает только static Lit `css\`\`` templates без `${…}`: - удаляет CSS comments вне строк; - схлопывает ASCII whitespace вне строк; - удаляет пробелы вокруг безопасной пунктуации, не меняя значения custom properties, strings, escapes, `url()`, `calc()`, media/container queries и descendant combinators; - fail-closed сообщает файл/позицию при interpolation или незакрытой строке/comment вместо частичной порчи CSS; - порядок templates и rules не меняется. Минификация не имеет отдельного продуктового budget: ожидаемая gzip-экономия мала и учитывается в общем initial View graph. ## 10. Fingerprint и cache contract 1. Source fingerprint продолжает покрывать `src/`, Rollup, TypeScript и lockfile; manifest несёт то же значение. 2. Entry публикует fingerprint как сейчас. Lazy runtime экспортирует тот же fingerprint; loader сверяет его до `ready`. 3. Demo freshness проверяет entry и manifest, затем SHA-256 каждого asset. 4. Content-hashed filename предотвращает выдачу нового кода из старого browser cache. Entry version query продолжает меняться вместе с release version. 5. Старый открытый entry после обновления может запросить удалённый hash; это штатный failure §6.4, а не основание хранить бесконечно старые chunks. ## 11. I18n и документация Добавляются четыре ключа (RU/EN): loading editor, load failed, refresh advice и доступное имя busy-state. Остальные подписи не меняются. Обновляются: - `docs/ARCHITECTURE.md` — multi-asset graph, runtime boundary и public routes; - `docs/DEVELOPMENT.md` — build/sync/deploy всего дерева, проверка manifest; - `docs/USER-GUIDE.ru.md` и README RU/EN — прежний resource URL сохраняется, но frontend состоит из entry и внутренних assets; ручное копирование одного JS не является поддерживаемой установкой; - `docs/TESTING.md`/релевантный testing guide — сценарий failure и network gate; - оба changelog — изменение пользовательски заметно по скорости и редкому сообщению ошибки. ## 12. Совместимость и миграция Stored data не меняется; миграции нет. Старый браузер, способный исполнять текущий ESM bundle, способен исполнять native dynamic import. При отсутствии assets обновлённый entry сохраняет View и выдаёт §6.4. Обновление integration требует полный HACS zip и обычный restart/reload HA. Новый backend с новым frontend является штатной парой; mixed-version runtime блокируется fingerprint handshake. ## 13. Критерии приёмки **AC1. Честный initial budget.** На production build сумма gzip entry и всех транзитивных static imports до рабочего первого View ≤ 256 000 B; dynamic editor assets не учитываются только если browser smoke подтверждает, что они не запрошены до editor intent. **Доказательство:** `bundle-budget` unit + CI command с manifest + Playwright network smoke. **AC2. Реальная lazy boundary.** Initial import graph не содержит editor runtime, editor toolbar/gesture/dialog modules и GUI config editor; первый вход в любой из трёх редакторов загружает один deduplicated editor graph. **Доказательство:** manifest/import-graph unit и Playwright network smoke. **AC3. View parity.** Configured View, kiosk, touch View, device actions, Glow/sun/vacuum, openings, room hover, space switching and visual continuity проходят без golden delta. **Доказательство:** связанные smokes + полный golden verify на pre-release gate; на code-review — selected smokes из registry. **AC4. Editor parity.** Plan, Devices и Background открываются после cold load; основные select/draw/save/undo flows и editor-to-editor transitions проходят существующие smokes без изменения ожидаемого DOM/данных. **Доказательство:** selected editor smokes и unit tests controller/loader. **AC5. Loader atomicity.** Двойной клик/конкурентные запросы выполняют один import/install; mode и camera меняются только после ready. **Доказательство:** unit с controllable Promise + browser smoke. **AC6. Failure сохраняет View.** Первый 404/parse/fingerprint failure retry-ится один раз; второй оставляет mode=view, план интерактивным и показывает локализованное сообщение. **Доказательство:** unit loader state machine + Playwright route abort/mismatch scenarios RU/EN. **AC7. Asset security.** Backend отдаёт только manifest-listed JS из asset root и отказывает traversal, encoded traversal, nested path, unknown extension и stale asset. **Доказательство:** HA backend tests. **AC8. Полнота distribution.** Build/sync/zip/release verification падают при удалении или подмене любого manifest asset; dist, integration и demo после sync побайтово совпадают по manifest. **Доказательство:** Node release-contract, bundle-sync и freshness tests + zip test. **AC9. CSS без семантической порчи.** Plugin корректно сохраняет strings, escapes, URLs, custom properties, calc/media/container и combinators, умеет падать на interpolation/malformed input; computed styles выбранных компонентов и golden images не меняются. **Доказательство:** unit с adversarial fixtures + computed-style smoke + golden verify. **AC10. Fingerprint mismatch не смешивает версии.** Runtime с другим fingerprint не устанавливается и проходит failure contract. **Доказательство:** unit + browser injected mismatch. **AC11. Onboarding и GUI editor.** Empty-config onboarding работает без editor asset; async `getConfigElement()` загружает editor graph и возвращает прежний custom element/config contract. **Доказательство:** browser smokes обоих путей. **AC12. Без model drift.** No-op config/layout roundtrip и backend validation не изменяют данные; `any` count в `src/` не растёт; второй geometry authority не появляется. **Доказательство:** existing roundtrip/schema tests + static gate. **AC13. Документация не обещает single-file install.** Resource URL остаётся прежним, а install/deploy docs требуют целое asset tree. **Доказательство:** `check-docs` и ревью текста. ## 14. План автотестов и гейты реализации На каждом implementation slice: ```text npm run typecheck npm test npm run build npm run bundle:sync node scripts/check-docs.mjs node scripts/smoke-select.mjs --base origin/dev --head HEAD <все smokes, выбранные registry и перечисленные AC> git diff --check ``` Перед code review дополнительно: - backend pure tests и полный HA subset в WSL/CI для frontend asset view; - new network/failure/editor cold-load smoke; - `npm run inventory`; - hashes полного asset tree; - `npm run golden:verify` как диагностический локальный прогон; канонический full golden/performance остаётся pre-beta CI по `PROCESS.md`. Mutation guards обязаны уметь сломать: dynamic boundary, retry cap, fingerprint check, manifest allowlist, omitted zip chunk, CSS string/comment handling и initial budget. ## 15. Риски и меры | Риск | Мера | |---|---| | Big-bang перенос editor state ломает жесты | переносить законченными typed slices; одинаковые smokes до/после каждого slice; без новой geometry model | | Chunk доступен в demo, но 404 в HA | production backend route + HA harness test; smoke использует production URL layout | | Entry мал, но eager shared chunk возвращает вес | budget следует transitive static graph из manifest и подтверждается network trace | | Update смешивает версии | content hash + source fingerprint handshake; отказ до install | | Сломан ручной deploy | sync/deploy docs и script работают с tree, не с одним `scp` | | CSS whitespace меняет selector/value | token-aware conservative transform, adversarial tests, computed style + golden | | Release zip неполон | manifest-driven zip validation и публичный asset verification | | First editor click выглядит зависшим | 150 ms delayed loading surface, View остаётся на месте | ## 16. Откат Откат выполняется одним release commit к предыдущему monolithic Rollup output, точному static route и single-file bundle tooling. Stored config/layout не менялись, поэтому data rollback не нужен. Backend не удаляет старый entry URL; после отката новые chunk routes становятся неиспользуемыми. ## 17. Release-артефакты - production entry + manifest + chunk tree в `dist/` и integration snapshot; - обновлённый `houseplan.zip` с полным tree; - прежний top-level GitHub `houseplan-card.js` entry asset; - RU/EN changelog со ссылкой на #337; - architecture/development/user docs; - новые unit/backend/browser tests; golden baselines не меняются без отдельного reviewed acceptance. ## 18. Принято предположительно, можно менять без владельца - имена внутренних modules/chunks и форма typed host port; - формат manifest, если он остаётся детерминированным и доказывает все AC; - точная реализация delayed loading surface поверх существующей transition UI; - механизм перечитывания manifest backend view; - gzip implementation и fixed compression level в budget script; - разбиение editor extraction на внутренние commits/slices. Нельзя менять без продуктового решения владельца: прежний resource URL, отсутствие автоматического hard reload, сохранение рабочего View при failure и отсутствие заранее загружаемого editor prefetch.