mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
724 lines
50 KiB
Markdown
724 lines
50 KiB
Markdown
# #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.
|