docs: specify lazy editor bundle

Issue: #337
User-Visible: no
This commit is contained in:
Matysh
2026-08-28 05:40:28 +00:00
committed by claude[bot]
parent 7340622524
commit 4485417027
+419
View File
@@ -0,0 +1,419 @@
# ТЗ #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/
<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:
```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.