# #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** — новый ``, которому 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. создаёт ровно один `` за своё подключение; 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: `` — блок с 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.