Files
houseplan-card/docs/specs/337-lazy-editor-chunk.md
2026-08-28 05:40:28 +00:00

27 KiB
Raw Permalink Blame History

ТЗ #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 пишет дерево:

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 атомарно синхронизирует всё дерево, а не один файл:

  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:

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.