Files
houseplan-card/docs/specs/486-house-plan-panel.md
T

724 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #486 — Панель House Plan в боковом меню Home Assistant
- **Issue:** https://github.com/Matysh/houseplan-card/issues/486
- **Тип / приоритет:** feature / P1
- **Трек:** полный; задача вводит новую пользовательскую поверхность и меняет
backend lifecycle, frontend host, bundle graph, responsive/touch-контракт и
первый запуск
- **Оценка:** пользовательская ценность 10/10; ценность для разработки 9/10;
сложность 8/10; риск 8/10
- **Связано:** #40, #210, #437, #462; `docs/SCOPE.md`,
`docs/UX-MODES.md`, `docs/TOUCH-SUPPORT.md`,
`docs/CONFIG-COMPATIBILITY.md`
## 1. Сценарий
Персона — новый Home admin, который установил House Plan через HACS и добавил
интеграцию, либо действующий пользователь, которому нужно изменить план. Момент
— сразу после setup интеграции или при следующем обслуживании дома.
Пользователь ожидает увидеть House Plan как приложение в боковом меню Home
Assistant. Сейчас ему нужно отдельно догадаться создать dashboard, добавить
custom card, найти редакторы внутри неё и затем работать в ширине одной колонки
Sections/Masonry. На большом мониторе такая колонка может оставлять редактору
около 400 CSS px и обрывать первый пользовательский путь ещё до первой комнаты.
## 2. Что человек увидит до и после
**До:** успешная установка интеграции не даёт очевидной точки входа. Редактор
доступен только после ручного добавления карточки; по умолчанию карточка в
Sections может оказаться узкой.
**После:** после setup в стандартном боковом меню HA появляется пункт
**House Plan** с иконкой `mdi:floor-plan`. Он открывает `/houseplan`: обычную HA
страницу, где тот же план занимает всю доступную ширину и высоту. В верхней
строке страницы остаётся название продукта и стандартный доступ к меню HA, а
ниже — пространства, редакторы и действия House Plan. Dashboard-card продолжает
работать как раньше; в Sections HA предлагает ей полную ширину по умолчанию.
Первое создание плана выполняется через уже существующий onboarding House Plan.
Пользователь без права записи видит тот же read-only View и не получает ложных
редакторских действий.
## 3. Подтверждённое текущее состояние
На исходной вершине `dev` задачи:
1. `custom_components/houseplan/__init__.py` регистрирует HTTP/WS глобально,
загружает server stores и подключает `houseplan-card.js`, но не регистрирует
HA panel.
2. `frontend_registration.py` надёжно регистрирует Lovelace resource, fallback,
versioned URL, одноразовое reload-уведомление и System Health для карточки.
Этот lifecycle следует переиспользовать, а не создавать второй resource.
3. `HouseplanCard` уже получает server-side `can_write`, соблюдает `admin_only`,
хранит последнее пространство и при уходе с HA route сбрасывает редактор в
View. Пустой writable-план уже запускает onboarding #40, но текущая empty
ветка ошибочно не проверяет `_canEdit`: read-only пользователь тоже видит
Add space, а runtime может автоматически открыть onboarding. Панель для всех
пользователей требует исправить этот разрыв в рамках #486.
4. Высота обычной карточки выводится из `100dvh` и измеренного HA chrome. Для
собственного полноэкранного host это неоднозначно: app bar и safe area могут
быть вычтены дважды. Нужен неперсистентный container-owned режим.
5. У full card есть `getCardSize()`, но нет `getGridOptions()`.
6. Rollup, bundle manifest и stale-entry fallback предполагают один entry и
выбирают первый `isEntry`. Простое добавление второго entry может поменять
authority, initial graph или оставить старый panel bundle в release.
7. Публичный совместимый API существует в HA 2024.6:
`panel_custom.async_register_panel`. Более новые `async_panel_exists`,
`handle_safe_area`, `sidebar_default_visible`, `show_in_sidebar` и keyword
`warn_if_unknown` у `async_remove_panel` нельзя считать доступными на
минимальной версии.
## 4. Продуктовые решения
1. Каноническая точка входа для создания и обслуживания плана — панель House
Plan. Dashboard-card остаётся необязательным способом встроить план в
пользовательский dashboard.
2. Панель и все dashboard-card используют одну server-side модель. У панели нет
отдельного плана, пространства, layout либо набора настроек.
3. Панель общедоступна всем аутентифицированным пользователям
(`require_admin=False`). Видимость редакторов и право записи определяются
только существующим `can_write`/`admin_only`; sidebar visibility не является
авторизацией.
4. Панель создаёт full card с чистыми defaults и никогда не включает card
`kiosk`. HA kiosk/скрытый sidebar может менять только shell страницы, но не
права или режим карточки.
5. В app bar показывается бренд **House Plan**. Дублирующий product title внутри
card header скрывается только внутренним panel-host признаком; tabs
пространств, редакторы, zoom/settings и остальные действия не скрываются.
6. Панель не заменяет #437: настраиваемая сводная боковая панель рядом с планом
остаётся отдельной будущей функцией.
7. `getGridOptions() -> { columns: "full" }` входит в #486. Это fallback для
пользователя, который начинает с dashboard-card, и не меняет ручной выбор
ширины.
8. Empty state с Add space и автоматический onboarding доступны только при
подтверждённом праве записи. Read-only empty state не показывает активный
CTA и не загружает onboarding/editor runtime; правило одинаково в панели и
dashboard-card.
## 5. Принятые предположения
Эти решения приняты предположительно и могут быть свободно изменены владельцем
до реализации без признания ТЗ заблокированным:
- route — `/houseplan`, custom element — `houseplan-panel`, sidebar icon —
`mdi:floor-plan`;
- собственный app bar использует публичное событие `hass-toggle-menu`, а не
внутренний API конкретной версии `ha-top-app-bar-fixed`/`ha-menu-button`;
- высота app bar следует HA theme и touch target не меньше 44×44 CSS px; точное
число пикселей не является модельным контрактом;
- пользователю с правом записи и пустым планом сразу доступен существующий
onboarding, без нового промежуточного welcome-screen;
- существующее одноразовое frontend-уведомление #462 получает новый текст,
вместо создания второго persistent notification;
- HA-native пользовательские настройки порядка/видимости/sidebar title после
регистрации не перезаписываются интеграцией.
Открытых продуктовых вопросов нет.
## 6. Термины
- **Panel shell** — новый `<houseplan-panel>`, которому HA передаёт `hass`,
`narrow`, `route` и `panel`; он владеет app bar и одним full card.
- **Panel-host mode** — неперсистентный runtime-признак full card: контейнер
задаёт доступную высоту, внешний card chrome становится плоским и не
дублируется product title. Это не `CardConfig` и не YAML option.
- **Card entry** — стабильный `houseplan-card.js`, Lovelace resource и authority
существующей dashboard-card.
- **Panel entry** — отдельный стабильный `houseplan-panel.js`, который HA
загружает лениво только при открытии панели.
- **Owned panel** — exact registry object, созданный текущим успешным setup
generation House Plan. Только его разрешено удалять при unload.
- **Foreign collision** — существующая панель другого владельца на route
`houseplan`.
## 7. Скоуп
1. Зарегистрировать sidebar panel через публичный HA API и безопасно обслуживать
setup, reload, disable, unload и remove.
2. Добавить отдельный frontend entry и manifest/release tooling для двух entry
без роста initial graph обычной карточки.
3. Реализовать panel shell, container-owned layout и внутренний panel-host mode
full card.
4. Сохранить существующие права, onboarding, состояние пространств и правило
сброса editor при уходе с route.
5. Исправить empty-state permission gap: Add space и onboarding только writer.
6. Добавить `getGridOptions()` только full card.
7. Обновить config-flow completion, существующее reload-уведомление, System
Health и переводы EN/RU/DE/FR.
8. Добавить автоматические backend/unit/browser/golden доказательства и
пользовательскую/архитектурную документацию.
## 8. Не входит
- отдельный server config для title, icon size, стартового пространства или
других настроек панели;
- перенос CardConfig dashboard-card на сервер или синхронизация нескольких
разных YAML-конфигов карточек;
- новый onboarding, конвертер, sample plan или изменение #40;
- kiosk mode панели, новый fixed-floor route либо deep routing внутри панели;
- редизайн редакторов, card header, навигации пространств или инструментов;
- полная поддержка редакторов на touch: остаётся сознательная деградация из
`docs/TOUCH-SUPPORT.md`;
- изменение пользовательского размера уже размещённых dashboard-card;
- изменение #437 либо добавление сводной панели рядом с планом;
- запись в private `.storage/frontend.panels` и принудительное восстановление
удалённого пользователем пункта sidebar;
- изменение внешнего landing-сайта или `llms.txt`: таких поверхностей в
текущем репозитории нет; README и User Guide являются канонической
пользовательской документацией #486, а внешний сайт при его появлении
получает отдельную задачу в своём репозитории;
- отдельный Lovelace resource или `extra_module_url` для panel entry;
- секреты, plan/config или instance-specific данные внутри публичного JS.
## 9. Backend-контракт
### 9.1 Регистрация
После успешной загрузки stores, миграций, repair и housekeeping
`async_setup_entry`:
1. убеждается, что `houseplan-panel.js` существует в установленном frontend;
2. регистрирует exact static URL `/houseplan_files/houseplan-panel.js` тем же
compatibility helper, что используется для card: async API на новых HA и
synchronous fallback на HA 2024.6;
3. вызывает:
```python
await panel_custom.async_register_panel(
hass=hass,
frontend_url_path="houseplan",
webcomponent_name="houseplan-panel",
sidebar_title="House Plan",
sidebar_icon="mdi:floor-plan",
module_url=f"/houseplan_files/houseplan-panel.js?v={VERSION}",
embed_iframe=False,
trust_external=False,
require_admin=False,
)
```
4. не передаёт `config_panel_domain`, `handle_safe_area`,
`sidebar_default_visible`, `show_in_sidebar` или другие параметры новее
minimum HA;
5. после успеха получает exact созданный registry object и только тогда
фиксирует ownership текущего generation;
6. регистрирует синхронный cleanup через `entry.async_on_unload`.
Панель регистрируется последней, чтобы поздний отказ миграции/repair не оставил
sidebar entry от неуспешного setup. Отказ panel setup, напротив, fail-soft: он
не отменяет уже рабочие stores, WS/HTTP и dashboard-card.
### 9.2 Static path
Process-wide состояние static registration становится per-URL set/map.
Переход в памяти с legacy boolean безопасен: `True` означает, что зарегистрирован
только `FRONTEND_URL` карточки; panel URL всё равно проверяется и регистрируется.
Static routes невозможно снять публичным HA API и они намеренно живут до
restart Core. Повторный reload entry не регистрирует тот же exact URL ещё раз.
Каталог интеграции целиком публично не публикуется.
### 9.3 Ownership, collision и generations
- `panel_custom.async_register_panel()` в HA 2024.6 возвращает `None`, а
публичный remove API умеет удалять только по route. Ради fail-closed cleanup
разрешено одно узкое чтение private in-memory registry
`hass.data[frontend.DATA_PANELS]`: объект не мутируется напрямую, private
`.storage/frontend.panels` не читается и не пишется. Эта зависимость явно
покрывается compatibility tests на minimum/current HA.
- Если сразу после собственной успешной регистрации registry/identity нельзя
прочитать, setup в том же синхронном участке снимает только что созданный
route публичным `async_remove_panel`, фиксирует `ownership_error` и оставляет
интеграцию без панели. Неидентифицируемая панель не остаётся до unload.
- Коллизия route поднимает/нормализуется в статус `collision`; чужая панель не
обновляется, не скрывается и не удаляется.
- Cleanup сравнивает текущий runtime generation и identity registry object с
сохранённым owned object. Старый callback не может снять новую House Plan
generation, а внешняя замена route не может быть удалена как наша.
- Используется двухаргументный `frontend.async_remove_panel(hass, "houseplan")`,
совместимый с HA 2024.6. `warn_if_unknown` не передаётся.
- Reload: owned panel снимается один раз, затем новая generation регистрируется
один раз. Disable/unload: sidebar entry исчезает. Uninstall после unload
идемпотентен и не делает второе слепое удаление.
- Missing bundle, static registration error, `ValueError` collision и иное
исключение panel API записываются безопасно, но setup интеграции возвращает
success.
### 9.4 Наблюдаемость
Runtime panel state минимум содержит:
- `panel_file_present`;
- `panel_static_path_registered`;
- `panel_status`: `not_attempted`, `registered`, `missing_asset`, `collision`,
`static_error`, `registration_error`, `ownership_error`, `removed`;
- `panel_url` — `/houseplan` либо `unavailable`;
- `panel_module_url` — versioned local URL либо `unavailable`;
- `panel_error` — безопасный fingerprint `phase:ExceptionType` либо `none`;
- internal generation/owned identity, не выводимые как сериализованные данные.
System Health добавляет эти поля к существующему frontend/resource состоянию.
Логи не включают exception message, пользовательский config, filesystem paths,
токены или содержимое плана. Collision получает понятный warning с route и
советом открыть System Health; отдельный Repairs issue в #486 не вводится.
## 10. Доступ и безопасность
`require_admin=False` определяет только присутствие пункта в списке панелей HA.
Для каждого пользователя full card получает обычный `hass` и server response:
- read-only пользователь видит View, но не editor tabs/изменяющие действия;
- администратор при `admin_only=true` получает существующее право записи;
- разрешённый non-admin при `admin_only=false` получает ровно существующий
`can_write`;
- backend WebSocket/HTTP guards остаются authority и не полагаются на скрытый
UI.
Panel JS и card JS публичны как обычные frontend resources. В них запрещены
access tokens, план, entity states и server config; всё instance-specific
приходит только через аутентифицированный HA runtime.
## 11. Bundle и release-контракт
### 11.1 Две именованные точки входа
Rollup получает именованные entries:
- card → стабильный `houseplan-card.js`;
- panel → стабильный `houseplan-panel.js`.
Конфигурация root не оставляется на порядок enumeration:
```js
input: {
'houseplan-card': 'src/houseplan-card.ts',
'houseplan-panel': 'src/houseplan-panel.ts',
},
entryFileNames: '[name].js',
```
`src/houseplan-panel.ts` статически подключает card entry и регистрирует только
`houseplan-panel`. Rollup должен переиспользовать один card graph, а не собрать
его вторую копию. Editor/onboarding/locale/isometric/PDF chunks сохраняют
существующую ленивость.
### 11.2 Manifest authority
Схема расширяется аддитивно и сохраняет действующие поля карточки:
- `entry: "houseplan-card.js"` остаётся authority для всех старых consumers;
- `panelEntry: "houseplan-panel.js"` явно задаёт второй root;
- `initialViewFiles`/`initialViewGzipBytes` считаются только от card entry;
- `initialPanelFiles`/`initialPanelGzipBytes` считаются от panel entry;
- `initialPanelOnlyFiles = initialPanelFiles \\ initialViewFiles`, а
`initialPanelOnlyGzipBytes` — сумма только этой разницы;
- все output files, включая оба entries и общие hashed chunks, присутствуют в
manifest inventory с digest/bytes/gzip и `isEntry:true` ровно у двух
объявленных стабильных roots.
`initialViewFiles` является подмножеством `initialPanelFiles`: panel shell
переиспользует card graph, а не дублирует его. Выбор «первый `isEntry`»
запрещён. Root определяется по exact filename и `facadeModuleId`.
### 11.3 Синхронизация и stale build
- На успешной сборке оба стабильных entry получают fail-loud stale-load
поведение: недоступный hashed implementation chunk даёт видимую просьбу
перезагрузить страницу, а не пустую поверхность. Compiler/Rollup failure
завершается non-zero; неполная сборка не синхронизируется и не публикуется.
- `bundle-sync` материализует полное дерево для репозитория, demo и release
package: hashed dependencies, оба стабильных entry, manifest и удаление
старого inventory. Эта последовательность **не объявляется live-atomic** для
уже обслуживаемой HA: в коротком окне замены entry новый chunk может ещё не
входить в старый manifest allowlist, и именно stale-load UI является
допустимой fail-loud деградацией до завершения sync/reload.
- freshness/tree/release/zip проверки валидируют оба roots и запрещают orphaned
либо missing panel files. Unlisted root-level `houseplan-*.js` также считается
orphan, а не только файл в `houseplan-assets/`.
- Panel entry не создаёт Lovelace resource и загружается HA лениво только при
открытии `/houseplan`.
- `houseplan-panel.js` входит в HACS zip. Самостоятельный GitHub asset
`houseplan-card.js` остаётся card-only; второй standalone asset в #486 не
вводится.
### 11.4 Бюджет
Действующий ceiling initial View карточки не увеличивается ради panel shell и
не ослабляется. `initialViewFiles` не содержит panel entry. Ceiling **8 KiB
gzip** применяется к `initialPanelOnlyGzipBytes`, а не ко всему panel closure:
общий panel graph включает тот же card graph, но не дублирует его.
Изменения самой full card (`getGridOptions`, panel-host branch) остаются внутри
действующего card budget. Если он исчерпан, код уменьшается или выносится, а
бюджет не повышается в #486.
## 12. Frontend panel shell
### 12.1 Lifecycle и свойства HA
`houseplan-panel` принимает property setters `hass`, `narrow`, `route`, `panel`.
Порядок присваивания произвольный. Shell:
1. создаёт ровно один `<houseplan-card>` за своё подключение;
2. до первого `hass` вызывает `setConfig({ type: "custom:houseplan-card" })`
ровно один раз;
3. передаёт каждое новое `hass` тому же child, не перемонтируя plan;
4. применяет `narrow`/`route`/`panel` к shell без повторного `setConfig`;
5. после disconnect не оставляет document/window listeners, observers или
timers; reconnect того же элемента не размножает child/listeners.
Panel config не преобразуется в публичный CardConfig. `hass.kioskMode`, HA
sidebar state или `narrow` никогда не превращаются в `config.kiosk`.
### 12.2 App bar и меню
App bar содержит:
- кнопку меню на всех ширинах; обычная пользовательская активация кнопки
(trusted исходный click/tap или keyboard activation) отправляет синтетическое,
поэтому само по себе неизбежно `isTrusted=false`, но bubbling + composed
событие `hass-toggle-menu`. Поэтому доступ к drawer не зависит от того,
передаёт ли конкретная версия HA отдельный desktop-collapsed flag; `narrow`
меняет только responsive presentation shell;
- заголовок **House Plan**;
- семантический heading/toolbar без фальшивой навигационной ссылки.
Accessible name кнопки берётся из `hass.localize` с English fallback. Shell не
зависит от private methods внутренних HA components. Если HA сам скрывает
sidebar в kiosk, это не включает card kiosk и не открывает редакторы.
### 12.3 Full-page layout
Shell — grid/flex из `appbar auto` и `content minmax(0, 1fr)`:
- занимает `width:100%` и высоту **viewport**: `100vh`, затем
`calc(100dvh − safe-area-inset-top − safe-area-inset-bottom)` (уточнение
#488: `<ha-panel-custom>` — блок с safe-area-паддингами и без высоты, поэтому
`height:100%` резолвится в `auto`, и стейдж схлопывается в 0 px; HA для
iframe-панелей по той же причине берёт `100dvh`);
- не создаёт page/horizontal scroll;
- content и child имеют `min-width:0`, `min-height:0`, `overflow:hidden`;
- паддинги safe-area, которые ставит сам `ha-panel-custom`, вычитаются из
высоты `:host` ровно один раз — сверху и снизу; боковые уже учтены шириной;
- outer card border/radius/shadow убираются только в panel-host mode.
Порядок монтирования у HA (#488): `ha-panel-custom` создаёт элемент и
присваивает `panel`/`hass`/`narrow`/`route` сразу после `load` module-скрипта,
а entry панели определяет класс после top-level `await import('./houseplan-card.js')`.
Значения ложатся собственными свойствами инстанса и затеняют accessors;
`houseplan-panel` в конструкторе и `connectedCallback` переприсваивает их через
accessors (`_adoptPreUpgradeProperties`). Смок `smoke_houseplan_panel.mjs`
воспроизводит именно этот порядок и контейнер без высоты; два мутанта держат
оба контракта.
Panel-host mode считает stage от **измеренного контейнера**, вычитая только
собственный card header/editor chrome. Он не использует `100dvh − HA chrome`.
Существующий ResizeObserver получает итоговый stage rect и выполняет обычный
refit. Смена space, View ↔ editor, narrow, sidebar, split-view, orientation и
virtual keyboard не дают отрицательной/нулевой высоты, snap или постоянного
resize-loop.
Обычные dashboard/kiosk/space-card размеры остаются неизменными.
### 12.4 Card header
В panel-host mode скрывается только product-title fragment. Если header
содержит tabs пространств, editors, device count, zoom/settings или контекстные
действия, они остаются и занимают измеряемую строку. В empty/fixed/error ветках
не остаётся пустой title-row: app bar уже является видимым и доступным
заголовком страницы.
## 13. Состояния и навигация
| Состояние | Ожидаемое поведение панели |
|---|---|
| План существует, writer | Последнее доступное пространство, View; editors доступны |
| План существует, read-only | То же пространство и View; editors отсутствуют |
| Пустой план, writer | Существующий onboarding/создание первого пространства |
| Пустой план, read-only | Read-only empty state без Add space, автодиалога и onboarding/editor runtime |
| Последнее пространство удалено | Существующий deterministic fallback пространства |
| Уход `/houseplan` → другой HA route | Editor/dialog/draft завершаются по действующему route-departure contract; сохраняется только пространство |
| Возврат на `/houseplan` | Новая panel/card instance открывает сохранённое пространство в View |
| Same-route технический remount | Действующий короткий warm-remount может сохранить непрерывность; это не считается уходом пользователя |
| Entry reload | Панель снимается и возвращается; browser может потребовать уже существующий hard reload после смены frontend version |
| Foreign collision | Чужая панель остаётся; House Plan dashboard-card и backend работают |
| Missing/broken panel bundle | Sidebar panel не регистрируется; dashboard-card и backend работают |
Panel route не вводит новое хранилище навигации. Действующий last-space browser
state общий с full card; editor mode не записывается.
## 14. Grid contract dashboard-card
`HouseplanCard` получает instance method:
```ts
getGridOptions(): { columns: 'full' }
```
Ограничения:
- метод только у `custom:houseplan-card`, не у `houseplan-space-card`;
- `rows`, `min_rows`, `max_rows` и принудительная высота не задаются;
- `getCardSize(): 12` сохраняется для Masonry и старых HA;
- HA, не знающая метод, его игнорирует;
- существующий вручную выбранный размер Sections не перезаписывается.
## 15. Первый запуск и i18n
### 15.1 Config flow
Успешный `async_create_entry` использует `description="panel_ready"` — этот
публичный контракт присутствует в HA 2024.6. Текст EN/RU/DE/FR:
- предлагает открыть House Plan в sidebar, если пункт появился;
- называет desktop рекомендуемым местом для редактирования;
- не обещает панель безусловно: если пункта нет, предлагает dashboard-card и
System Health, что покрывает collision/fail-soft. Безусловной ссылки на
`/houseplan` нет: при foreign collision этот route принадлежит другому panel.
### 15.2 Одноразовое уведомление #462
Стабильный ID и persisted flag не меняются. Текст существующего уведомления во
всех четырёх backend locales теперь сообщает, что:
1. House Plan доступен в боковом меню;
2. после update всё ещё нужен полный reload страницы;
3. dashboard-card остаётся доступна и при ручном управлении resource проверяется
Settings → Dashboards → Resources.
Существующий пользователь, уже получивший уведомление #462, не получает его
повторно только ради #486. Нового флага или второго уведомления нет.
Sidebar brand **House Plan** не локализуется. Текст карточки/onboarding следует
языку browser HA как сейчас; menu accessible label использует HA localization.
Точные ключи, обязательные одновременно в источнике и EN/RU/DE/FR:
- новый backend `config.create_entry.panel_ready`;
- существующие, но с обновлённым текстом
`issues.frontend_reload_notice.title` и
`issues.frontend_reload_notice.description`;
- новые System Health labels `system_health.info.panel_file`,
`system_health.info.panel_static_path`, `system_health.info.panel_status`,
`system_health.info.panel_url`, `system_health.info.panel_module_url`,
`system_health.info.panel_error`;
- новый frontend `empty.read_only`, объясняющий, что первое пространство может
создать пользователь с правом редактирования.
## 16. Модель, миграция и совместимость
- `CardConfig`, plan schema, layout, entities, markers, spaces и storage format
не меняются; config migration отсутствует.
- Существующие dashboard-card, kiosk, fixed-floor и space-card не меняются.
- После обновления существующая установка получает панель при следующем
setup/reload интеграции; план и browser last-space остаются.
- HA 2024.6 поддерживается без новых kwargs. Compatibility unit/stub должен
падать, если implementation начнёт их передавать.
- Browser с уже определёнными старыми custom elements может применить новый
frontend только после hard reload; действующий version/reload-контракт #462
остаётся authority.
- Нативные HA overrides sidebar title/order/visibility переживают unregister по
правилам HA и не чистятся через private storage.
## 17. Accessibility, touch и responsive
- Panel shell не ухудшает существующие pointer, keyboard и touch-контракты View
из `docs/TOUCH-SUPPORT.md`; #486 не подменяет отдельный аудит #31.
- Menu button и все app-bar действия имеют hit target не меньше 44×44 CSS px,
видимый focus и accessible name.
- App bar — landmark/header; title — доступный page heading. Скрытый внутренний
title не создаёт дублирующее объявление.
- Tab order начинается с menu и продолжается существующими House Plan controls;
retained header controls и уже доступные stage interactives сохраняют
focus-visible, accessible name и keyboard activation. Shell не делает
autofocus и не крадёт focus при `hass` update/refit.
- При 320 CSS px, tablet portrait/landscape, desktop wide, sidebar collapse и
mobile safe area отсутствуют horizontal scroll, обрезанная app bar и
недоступные View controls.
- Редакторы на touch остаются best-effort. Документация прямо рекомендует
desktop и не обещает полноценную touch-редактуру из-за новой панели.
- `prefers-reduced-motion` использует существующие правила карточки; shell не
добавляет самостоятельную декоративную анимацию.
## 18. Производительность
1. Регистрация panel entry не меняет cold-load dashboard-card и её Lovelace
resource.
2. До первого открытия `/houseplan` browser не загружает panel entry.
3. Populated panel View загружает panel shell + существующий initial card graph;
lazy editor/onboarding/locale/isometric/PDF chunks не становятся eager.
4. `hass` updates не пересоздают child, не повторяют `setConfig` и не создают
второй WebSocket subscription/render tree.
5. Container observer не образует write/read loop и не добавляет постоянный
layout read на каждый HA state tick.
6. Card ceiling не повышается; panel-only ceiling из §11.4 проверяется CI.
## 19. Acceptance criteria
- **AC1 (`backend` + mutation):** setup на поддерживаемом HA регистрирует ровно
одну `/houseplan` custom panel с exact element/title/icon/versioned module URL,
`require_admin=False`, без kwargs новее HA 2024.6; мутанты path, permissions,
module URL и API обязаны падать.
- **AC2 (`backend` + mutation):** unload/reload/disable/remove удаляют только
owned exact registry object и не оставляют duplicate; старая generation и
foreign collision не могут удалить/переписать текущую чужую или новую панель.
- **AC3 (`backend`):** missing panel file, static-path failure, collision и API
exception не роняют setup/card/WS; System Health честно различает все статусы
§9.4 и не раскрывает exception message/path/data.
- **AC4 (`unit` + bundle gate):** manifest явно различает card/panel entries,
inventories и graphs; card `entry`/initial semantics обратно совместимы;
оба stable-entry stale-load поведения, sync materialization, freshness, tree
и release zip проверяются отрицательными fixtures, включая unlisted
root-level `houseplan-*.js`.
- **AC5 (`unit` + bundle gate + performance + mutation):** panel entry
отсутствует в card initial graph,
`initialViewFiles ⊆ initialPanelFiles`, действующий card ceiling не повышен,
`initialPanelOnlyGzipBytes ≤ 8 KiB` и один card graph не продублирован;
eager-import/duplicate-graph mutant обязан уронить проверку.
- **AC6 (`unit` + smoke):** panel shell создаёт один child, делает один
`setConfig`, прокидывает все `hass` updates и реагирует на
`narrow`/`route`/`panel` без remount; disconnect/reconnect не размножает
listeners/child.
- **AC7 (`smoke` + golden):** wide View `1280×800`, wide Plan editor и narrow
read-only/empty `320×720` имеют `scrollWidth-clientWidth ≤ 1 CSS px`; stage
отличается от измеренного content-slot не более чем на 1 CSS px, остаётся
положительным и после одного resize/orientation intent стабилизирует rect в
пределах 0.5 CSS px за два последовательных animation frame. Нет двойного
title; fit envelope целиком внутри stage с действующим отступом. Эталоны
принимаются только независимым Linux capture/review.
- **AC8 (`smoke` + code review):** пользовательская активация menu на
wide/narrow dispatches bubbling + composed `hass-toggle-menu`; его
focus/ARIA/hit target корректны,
retained header и representative existing stage interactive остаются
keyboard-reachable; HA kiosk не превращается в card kiosk.
- **AC9 (`backend` + smoke):** admin и read-only пользователь видят panel;
editor visibility и реальные write calls по-прежнему следуют
`can_write`/`admin_only`, включая разрешённого non-admin.
- **AC10 (`unit` + smoke + mutation):** populated/empty writer/empty read-only и
missing last-space дают состояния §13; read-only branch не показывает CTA,
не открывает диалог и не импортирует onboarding/editor runtime; существующий
writer onboarding не дублируется.
- **AC11 (`unit` + smoke):** уход с panel route и возврат сохраняют только space
и открывают View; same-route technical remount сохраняет действующий короткий
continuity contract.
- **AC12 (`unit` + code review):** full card возвращает только
`{columns:"full"}`, сохраняет `getCardSize()`, space-card не получает новый
контракт. Метод объявляет только default; код House Plan не читает и не
мутирует сохранённый HA Sections layout, поэтому ручной размер остаётся
ответственностью host HA.
- **AC13 (`backend` + i18n gate):** config-flow completion и существующее
reload notice имеют согласованные EN/RU/DE/FR keys/text; существующий persisted
flag не создаёт повторное уведомление при upgrade.
- **AC14 (`unit` + backend + code review):** plan/config/layout schema и
`CardConfig` не меняются; публичные panel assets не содержат secrets/user data;
HA 2024.6 compatibility stub отвергает использование новых API/kwargs.
- **AC15 (`docs` + code review):** README EN/RU, User Guide EN/RU,
Architecture, UX Modes, Touch Support, Testing и Status описывают panel как
основной вход, dashboard-card как optional, desktop-first editing,
lifecycle/fail-soft диагностику и Sections default.
## 20. Test plan
### Backend HA harness / pure compatibility
1. Successful register + current panel registry contents.
2. Unload, reload, disable, remove and stale-generation callback.
3. Pre-existing foreign panel; external replacement after our register.
4. Missing panel bundle and failing static/register API.
5. `get_panels` visibility for admin/read-only and server write denial.
6. System Health matrix and safe error fingerprint.
7. Config flow `description=panel_ready`, notification persisted flag and all
locales.
8. HA 2024.6-shaped stubs without newer parameters/methods.
### Unit/tooling
1. Exact Rollup input `{ 'houseplan-card': 'src/houseplan-card.ts',
'houseplan-panel': 'src/houseplan-panel.ts' }`, `entryFileNames:'[name].js'`
and deterministic exact filename/facade root selection.
2. Card/panel dependency graphs, no duplication and budgets.
3. Both stable-entry stale-load behaviors; missing/wrong digest/orphan or
unlisted managed root-level entry fixture must fail.
4. Sync order and packaged frontend inventory.
5. Panel property/lifecycle/menu contract.
6. `getGridOptions` exact return and space-card absence.
### Browser smoke/golden
1. Cold `/houseplan` populated View writer, then first editor intent.
2. Empty writer onboarding and empty read-only state.
3. Permission matrix, no `config.kiosk` propagation.
4. Desktop wide, narrow 320 px, orientation/resize/sidebar collapse.
5. View → editor → editor swap → View; stage rect and scroll probes.
6. Route departure/return and same-route technical remount.
7. Menu event, keyboard focus and touch target probes.
8. Focused panel-host goldens from AC7 plus unchanged representative bare-card
golden.
## 21. Затронутые файлы и модули
Ожидаемый набор (имена helper/test могут уточняться без изменения контракта):
- `src/houseplan-panel.ts`, `src/houseplan-card.ts`, panel/card styles;
- `rollup.config.mjs`, `scripts/bundle-manifest.mjs`,
`scripts/bundle-sync.mjs`, `scripts/bundle-tree.mjs`, bundle budget/freshness,
`scripts/release-prerelease.mjs`, `.github/workflows/release.yml` и
`.github/workflows/publish-prerelease.yml` там, где они предполагают один
entry;
- `custom_components/houseplan/__init__.py`, `frontend_registration.py` либо
отдельный `panel_registration.py`, `frontend_asset_manifest.py`,
`system_health.py`, `config_flow.py`, `manifest.json`;
- `custom_components/houseplan/strings.json` и translations EN/RU/DE/FR;
- targeted `test/*.test.mjs`, `tests_backend/test_ha_*.py`, browser smoke и
golden matrix/baselines;
- `README.md`, `README.ru.md`, `docs/USER-GUIDE.md`,
`docs/USER-GUIDE.ru.md`, `docs/ARCHITECTURE.md`, `docs/UX-MODES.md`,
`docs/TOUCH-SUPPORT.md`, `docs/TESTING.md`, `docs/STATUS.md`;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` в пользовательском implementation
commit.
## 22. Риски и защита
| Риск | Вероятность / ущерб | Защита |
|---|---|---|
| Multi-entry меняет «первый entry» и initial budget | high / high | explicit roots, negative manifest fixtures, unchanged card ceiling |
| Release получает stale panel entry или missing chunk | medium / high | complete offline materialization, fail-loud stale-load behavior, zip integrity |
| Unload снимает чужую/новую panel | medium / high | generation + registry object identity, collision tests |
| App bar/safe area вычитаются дважды | high / high | container-owned sizing, responsive smoke/golden |
| Shell пересоздаёт card на каждом `hass` | medium / high | one-child/one-setConfig contract and subscription probe |
| `require_admin=False` ошибочно открывает writes | low / critical | existing backend auth authority + permission matrix |
| Внутренний HA component/API меняется | medium / medium | public panel helper/event only, minimum-HA stub |
| Двойная шапка съедает высоту | high / medium | panel app bar + hide only duplicated product title |
| Editor возвращается после ухода с route | medium / medium | existing route-departure integration smoke |
| Panel недоступна из-за пользовательского sidebar override | medium / low | direct `/houseplan`, docs; private storage не трогаем |
## 23. Rollback
Если panel lifecycle или frontend host даёт критическую регрессию:
1. перестать вызывать panel registration и снять owned `/houseplan` при unload;
2. оставить card entry, dashboard-card, server stores и plan schema без отката;
3. panel entry можно сохранить неиспользуемым один цикл или убрать вместе с
manifest field/tooling после проверки package inventory;
4. `getGridOptions` можно откатить независимо без изменения сохранённых cards;
5. frontend reload notification вернуть к нейтральному тексту без изменения
persisted flag.
Откат не требует миграции данных и не изменяет пользовательские планы.
## 24. Release-артефакты
- по одной значимой записи со ссылкой #486 в `docs/CHANGELOG.ru.md` и
`docs/CHANGELOG.md`;
- README EN/RU и User Guide EN/RU с первичным sidebar flow и optional card;
- внешний landing и `llms.txt` явно отложены по §8, потому что не принадлежат
этому репозиторию;
- Architecture/UX/Touch/Testing/Status по AC15;
- принятые panel-host golden и, если меняется документационный кадр, пересъёмка
через Linux workflow + `docs:accept -- --reviewed`;
- bundle manifest/tree/budget/freshness и HACS zip с обоими entries;
- targeted backend/unit/smoke/golden gates, затем обычный full-track S7 review;
- security evidence: public bundle inventory и неизменные server write guards;
- performance evidence: неизменный card ceiling и отдельный panel-only budget.