27 KiB
ТЗ #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. Цели
- Не загружать runtime Plan/Devices/Background и editor-only dialogs в первом рабочем View.
- Ограничить сумму gzip initial View graph величиной 250 KiB.
- Сохранить наблюдаемое поведение View, переходов, редакторов, конфигурации и Home Assistant card editor.
- Сделать несколько frontend-файлов полноценным проверяемым артефактом HACS, backend, demo, CI и release automation.
- Безопасно минифицировать статические 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
houseplan-cardиhouseplan-space-cardрегистрируются так же надёжно, как до изменения; HA не показывает промежуточноеCustom element doesn't exist.- View считается готовым только когда видны план и устройства и доступны обычные View-действия. Все JS-файлы, обязательные до этого момента, входят в initial View graph и budget §13.
- Ни один editor-only asset не запрашивается до намерения пользователя открыть редактор или editor-only диалог. Hover без клика и простои страницы не считаются намерением.
- Пустая новая установка сохраняет текущий onboarding: диалог создания первого пространства не может зависеть от editor runtime.
6.2 Вход в редактор
- Нажатие Plan, Devices или Background сначала запускает единственный shared loader. Mode, editor chrome и editor camera не коммитятся до успешной установки runtime.
- Повторные/конкурентные клики используют один Promise; runtime не скачивается и не устанавливается дважды.
- Пока загрузка занимает меньше 150 ms, отдельная плашка не появляется. После 150 ms поверх неизменившегося View показывается неблокирующий существующий transition surface с текстом «Загружаем редактор…» / “Loading editor…”.
- После успеха выполняется обычный переход текущего
mode-transition; первая editor-панель не появляется в полуготовом состоянии. Пространство, View zoom snapshot и selection contracts сохраняются. - После первой успешной загрузки все три редактора и editor-only dialogs используют уже установленный runtime без новой сети.
- 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 Ошибка загрузки и обновление во время открытой вкладки
- Первый сетевой/parse/fingerprint failure повторяет import один раз с versioned cache-buster текущей карточки. До retry View остаётся рабочим.
- После повторной ошибки mode остаётся
view, editor session не создаётся, toolbar/camera/selection не меняются. Показывается локализованное сообщение: «Не удалось загрузить редактор. Обновите страницу и повторите попытку.» / “Could not load the editor. Refresh the page and try again.” - Runtime сообщает build fingerprint до установки. Несовпадение entry/runtime рассматривается как тот же failure; код разных версий не смешивается.
- Автоматический hard reload запрещён: он может прервать другое действие на HA-дашборде. Пользователь сам решает, когда обновить страницу.
- Ошибка editor asset никогда не скрывает план и не ломает View-действия.
7. Архитектурный контракт frontend
7.1 Граница runtime
Создаётся src/editors/runtime/ с двумя явными сторонами:
- eager
editor-loader.ts: маленькая state machineidle → 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 пишет дерево:
dist/
houseplan-card.js
houseplan-assets/
<rollup chunks>.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 атомарно синхронизирует всё дерево, а не один файл:
dist/— build output;custom_components/houseplan/frontend/— committed release snapshot;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
- Source fingerprint продолжает покрывать
src/, Rollup, TypeScript и lockfile; manifest несёт то же значение. - Entry публикует fingerprint как сейчас. Lazy runtime экспортирует тот же
fingerprint; loader сверяет его до
ready. - Demo freshness проверяет entry и manifest, затем SHA-256 каждого asset.
- Content-hashed filename предотвращает выдачу нового кода из старого browser cache. Entry version query продолжает меняться вместе с release version.
- Старый открытый 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:
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.jsentry 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.