# #462 — Надёжная регистрация frontend-ресурса и восстановление после обновления - **Issue:** https://github.com/Matysh/houseplan-card/issues/462 - **Тип / приоритет:** bug / P1 - **Трек:** полный; меняются backend lifecycle, frontend recovery-controller и обязательный View/kiosk UX - **Оценка:** пользовательская ценность 10/10; ценность для разработки 8/10; сложность 7/10; риск 8/10 - **Связано:** #2, #295, #353, #412; `docs/SCOPE.md`, `docs/TOUCH-SUPPORT.md`, `docs/UX-MODES.md`, `docs/CONFIG-COMPATIBILITY.md` ## 1. Сценарий Администратор устанавливает House Plan через HACS, добавляет интеграцию и перезапускает Home Assistant. Либо обновляет уже установленную интеграцию. В открытой вкладке карточка отсутствует в picker или продолжает исполнять старый frontend bundle, хотя backend и файл новой версии уже доступны. Пользователь должен получить три независимых уровня помощи: 1. корректную инструкцию для своего режима Lovelace; 2. видимую диагностику способа подключения ресурса на стороне HA; 3. понятное восстановление при несовпадении уже загруженной карточки и backend. На настенной панели в kiosk обновление применяется само, но только в момент, когда перезагрузка не уничтожит действие пользователя и не создаст reload-loop. ## 2. Что человек увидит до и после **До:** документация предлагает невалидный верхнеуровневый `resources:`, storage-пользователь применяет YAML, который его dashboard игнорирует, отказ авторегистрации скрыт в логе, а старая вкладка выглядит как актуальная. Иногда помогает только угаданный `Ctrl+F5`. **После:** документация отдельно описывает storage и YAML; после первой доступной регистрации HA один раз сообщает, что нужно полностью перезагрузить frontend; System Health показывает файл, статический путь, способ регистрации и URL. Уже загруженная full card при несовпадении версий показывает компактную плашку с перезагрузкой. Kiosk выполняет не более одной тихой перезагрузки для конкретной целевой backend-версии и при неуспехе показывает ту же плашку. ## 3. Подтверждённая причина На `dev` `16b4c2d6` (`v1.72.0-beta.4`) подтверждены все исходные разрывы: - README и оба User Guide показывают плоский `resources:` без `lovelace:` и не разделяют storage/YAML mode; - `_register_lovelace_resource()` возвращает один `False` для отсутствующего реестра, YAML mode и исключения, поэтому lifecycle не может отличить устойчивый fallback от временной гонки; - fallback `add_extra_js_url()` невидим в UI и не ожидается Lovelace перед построением dashboard; - `manifest.json` не содержит `after_dependencies: ["lovelace"]`, повторной попытки нет; - System Health содержит только статистику плана; - backend уже отдаёт `integration_version`, а full card уже знает `CARD_VERSION`, но сравнение живёт в lazy editor runtime и не защищает View или kiosk; - браузер не может переопределить уже зарегистрированный custom element без reload документа. Кэш-бастинг `?v=` исправен и не является причиной. Полевой сценарий #462 завершился после `Ctrl+F5`, поэтому задача не меняет путь раздачи или стратегию кэш-бастинга: она делает существующий механизм надёжным и наблюдаемым. Гипотеза о штатной startup-race **не подтверждена**: и в минимально поддерживаемом HA 2024.6, и в актуальном HA 2026.8 hard dependency `houseplan → frontend → lovelace` должна подготовить `hass.data["lovelace"]` до setup entry. Явный `after_dependencies` документирует намерение и страхует изменение upstream-графа; one-shot retry восстанавливает искусственный transient отказ/исключение, но ни один release note не называет гонку установленной причиной полевого случая. Версионная граница YAML resources подтверждена первичными upstream-источниками, а не экстраполяцией по HA 2025.x: - Home Assistant Core PR [#161816](https://github.com/home-assistant/core/pull/161816), merged в milestone 2026.2.0 коммитом [`190fe10`](https://github.com/home-assistant/core/commit/190fe10), прямо «introduce a new key: `resource_mode` to replace `mode`» и отделяет загрузку ресурсов от режима dashboard; - исходник тега [2026.2.0](https://github.com/home-assistant/core/blob/2026.2.0/homeassistant/components/lovelace/__init__.py) содержит `CONF_RESOURCE_MODE`, отдельное поле `LovelaceData.resource_mode` и fallback на legacy `mode`; - [актуальная официальная документация HA](https://www.home-assistant.io/dashboards/dashboards/) требует `resource_mode: yaml` для `lovelace.resources` и описывает его отдельно от `dashboards.*.mode`. Поэтому HA до 2026.2 и HA 2026.2+ намеренно получают разные snippets; это не противоречит исходному репорту, который описывает storage mode старого HA. ## 4. Решения владельца 1. В обычном режиме автоматической перезагрузки нет. 2. В kiosk разрешена тихая перезагрузка только при безопасном состоянии. 3. Минимальные обязательные guards переиспользуют смысл `_cycleTick`: `Date.now() >= _cyclePausedUntil` и `_zoom <= 1.001`; дополнительно запрещены editor, dialog и незавершённые physical writes. 4. На одну целевую `integration_version` допустима ровно одна автоматическая перезагрузка в рамках browser-tab session. Отметка записывается до reload; смена/чередование frontend bundle при той же backend-версии не даёт новую попытку. 5. Если после неё версии всё ещё различаются, повтор запрещён и показывается ручная плашка. ## 5. Термины и границы - **Storage mode** — ресурсы управляются UI HA. Интеграция может создавать и обновлять запись Lovelace resource registry. - **YAML resources mode** — ресурсы объявляются под `lovelace.resources` в `configuration.yaml`; backend не пишет их в registry. В HA 2026.2+ режим выбирается независимым `lovelace.resource_mode: yaml` и не переводит сами dashboards из storage в YAML. - **Legacy full-YAML dashboard mode** — совместимый с поддерживаемыми HA 2024.6–2026.1 вариант `lovelace.mode: yaml`; он управляет не только ресурсами, но и самим dashboard, поэтому не предлагается пользователю storage dashboard как равнозначная замена современному `resource_mode`. - **Fallback** — подключение через `add_extra_js_url` при недоступном для записи registry. Это поддерживаемая деградация, но она требует reload документа. - **Совпадение версий** — точное равенство непустых строк `CARD_VERSION` и `integration_version`. Порядок semver не угадывается: frontend может быть как старее, так и новее backend. - **Неизвестная версия** — одна из сторон не предоставила корректную непустую строку. Она не считается mismatch и не запускает notice/reload. - **Full card** — `custom:houseplan-card`. Runtime-плашка и kiosk auto-reload относятся к ней. `custom:houseplan-space-card` загружается тем же bundle, но отдельный runtime banner/kiosk-контроллер в этой задаче не получает. ## 6. Скоуп 1. Исправление README EN/RU и английского/русского User Guide: установка, storage/YAML развилка и hard reload. 2. Типизированный результат регистрации ресурса вместо boolean. 3. `after_dependencies: ["lovelace"]`, одна lifecycle-bound повторная попытка после старта HA для временно недоступного registry и корректная отмена при unload. 4. Взаимоисключающий финальный loader: Lovelace registry либо `extra_module_url`; fallback удаляется, если retry успешно перешёл на registry. 5. Локализованное одноразовое persistent notification после первой доступной регистрации frontend-ресурса. 6. Состояние frontend-регистрации в System Health. 7. Runtime version mismatch controller в initial full-card bundle, без импорта lazy editor runtime. 8. Ручная плашка и безопасная одноразовая kiosk-перезагрузка. 9. i18n EN/RU/DE/FR, backend/unit/browser/mutation проверки, документация и два changelog. ## 7. Не входит - изменение `/houseplan_files/houseplan-card.js` или query cache busting; - горячая замена custom element без reload документа; - поддержка одиночного JS без установленной интеграции; - автоматическое изменение пользовательского `configuration.yaml`; - отдельная update-плашка и kiosk-режим для `houseplan-space-card`; - общий менеджер обновлений HACS/HA и проверка наличия новой версии в сети; - повторные бесконечные polling/backoff попытки Lovelace registry; - перестройка picker или dashboard UI Home Assistant. ## 8. Backend-контракт регистрации ### 8.1 Типизированный результат Одна функция регистрации возвращает структурированный outcome, достаточный для диагностики и решения lifecycle, минимум со следующими состояниями: | Outcome | Смысл | Финальный loader | |---|---|---| | `created` | запись resource создана | `lovelace_resource` | | `updated` | URL существующей записи обновлён до текущей версии | `lovelace_resource` | | `existing` | точный URL уже существовал | `lovelace_resource` | | `registry_pending` | registry ещё не появился/не загрузился | временно fallback, затем один retry | | `yaml_fallback` | registry сознательно недоступен для записи | `extra_module_url` | | `transient_error` | первая попытка упала на load/create/update | временно fallback, затем один retry | | `error_fallback` | повторная попытка также упала | `extra_module_url` | Outcome включает безопасный короткий `last_error` только для диагностики; stack trace, токены и пути вне config не попадают в System Health. ### 8.2 Lifecycle и retry 1. Статический путь регистрируется как сейчас, один раз на HA run. 2. При наличии файла выполняется первая попытка registry. 3. `created/updated/existing` завершают путь без `extra_module_url`. 4. `registry_pending` или первый `transient_error` включает fallback немедленно и ставит ровно одну отложенную попытку: штатный HA start helper дожидается running-state, после чего cancellable lifecycle timer даёт registry ещё одну фиксированную bounded паузу в 1 секунду. Если HA уже running, start helper вызывается сразу, но секундная пауза всё равно сохраняется — retry не должен повторять transient отказ в том же tick. 5. Отмена start-listener, timer и уже запущенной retry task регистрируется через `entry.async_on_unload`. Дополнительный cancellation/generation guard перед каждым поздним side effect гарантирует, что unload/remove не воскресит удалённую интеграцию даже при гонке с уже начавшейся coroutine. 6. Успешный retry удаляет через штатный frontend helper только тот exact versioned URL, который этот setup сам добавил в `extra_module_url`, и фиксирует registry как финальный loader. Чужие URL не затрагиваются. 7. Неуспешный retry превращает состояние в устойчивый `error_fallback`. Нового timer, tight loop и накопления listeners нет. 8. `yaml_fallback` и `error_fallback` не создают бесконечный retry. Повторный setup остаётся идемпотентным. 9. Несколько найденных legacy resource entries не размножаются: authority — одна каноническая запись; существующее best-effort удаление при uninstall очищает все записи с base URL. Если HA API текущей минимально поддерживаемой версии не предоставляет удаление `extra_module_url`, реализация не создаёт два разных URL: это фиксируется как `lovelace_resource_with_session_fallback` в диагностике до следующего reload, а registry остаётся authority для будущих документов. Предпочтителен доступный штатный `remove_extra_js_url`. ### 8.3 Наблюдаемое runtime-состояние В `hass.data[DOMAIN]` хранится только состояние текущего run: - `card_file_present`; - `static_path_registered`; - `resource_status` — последний outcome; - `loader` — `lovelace_resource`, `extra_module_url`, `lovelace_resource_with_session_fallback` или `none`; - `module_url` — точный versioned URL; - `retry_pending` и `retry_attempted`; - безопасный `last_error` либо `null`. Отсутствующий bundle не валит setup: `loader=none`, файл `false`, warning и честный System Health. Ложное значение `static_path_registered=true` до успешного вызова HA API запрещено. ## 9. Одноразовое уведомление после первой регистрации Используется локализованное `persistent_notification` со стабильным namespaced ID. Это информационный onboarding, а не сохраняемая неисправность: backend не пытается определять, какую из вкладок пользователь уже обновил, и не создаёт Repairs issue. - Уведомление создаётся ровно один раз за жизнь config entry после первой доступной регистрации frontend: `created`, `updated`, `existing` или поддерживаемый fallback при наличии файла и зарегистрированного static path. - Флаг «уведомление уже создано» записывается в служебное поле config entry сразу после того, как синхронный callback `persistent_notification.async_create` вернулся без исключения. Он не зависит от backend `VERSION`. - Существующая установка без поля получает одно уведомление при первом setup версии с #462. После этого update/downgrade/reload entry его не возвращают. - Dismiss уведомления не влияет на интеграцию. После restart HA оно может исчезнуть по правилам persistent notification и не создаётся повторно: это намеренно одноразовая инструкция, а не контроль прочтения. - Отсутствующий frontend-файл либо неуспешный static path не устанавливает флаг: уведомление появится при первом последующем setup, где frontend доступен. - Повторный setup, параллельное завершение retry и несколько entries не создают дубли: стабильный notification ID и persisted flag являются authority. - Uninstall dismiss-ит уведомление best effort; служебное поле исчезает вместе с entry. - Текст сообщает: карточка подключена; полностью перезагрузите страницу (`Ctrl+F5` / `Cmd+Shift+R`); при ручной настройке storage dashboard ресурс находится в Settings → Dashboards → Resources. Текст берётся через HA `async_get_translations` из совместимой с HA 2024.6 категории `issues` backend translation catalog для языка HA с fallback на English; EN/RU/DE/FR поставляются одновременно. Использование переводов из `issues` не превращает сообщение в Repairs issue: показ выполняет только `persistent_notification`. Это глобальное системное уведомление и поэтому использует язык экземпляра HA, а не язык конкретной открытой вкладки. ## 10. System Health `system_health_info()` сохраняет существующую статистику и добавляет локализованные ключи: | Поле | Значение | |---|---| | `card_file` | `present` / `missing` | | `static_path` | `registered` / `not_registered` | | `resource_status` | последний типизированный outcome либо `not_attempted` | | `resource_loader` | финальный loader из §8.3 | | `resource_url` | точный `FRONTEND_URL?v=VERSION` либо `unavailable` | | `resource_retry` | `pending` / `attempted` / `not_needed` | | `resource_error` | безопасная причина либо `none` | | `first_reload_notice` | `created` / `already_created` / `pending_frontend` | Ключи `system_health.info.*` добавляются в `strings.json` и EN/RU/DE/FR translations по контракту HA. Значения остаются короткими и пригодными для копирования в support report. System Health не обещает, что конкретная вкладка браузера уже перезагружена: одноразовый флаг подтверждает только создание инструкции, а фактическое состояние показывает runtime-сравнение. ## 11. Frontend version controller ### 11.1 Источник истины Контроллер находится в initial graph full card, использует только `CARD_VERSION` и последний корректный `integration_version` из успешного `config/get`. Lazy editor runtime не загружается ради проверки. Каждый успешный `config/get` авторитетен. Версия считается известной только если поле имеет тип string и после `trim()` не пусто; сравнивается нормализованная trimmed string без предположений о SemVer. Любой другой тип, пустая/whitespace строка или отсутствующее поле очищает ранее принятое значение до `unknown`, а не оставляет stale mismatch от предыдущего ответа/reconnect. Матрица: | Frontend | Backend | Обычный режим | Kiosk | |---|---|---|---| | unknown | любой | ничего | ничего | | `A` | `A` | ничего | ничего | | `A` | `B`, target `B` не пытался | плашка, только ручной reload | без плашки; при safe state одна auto-попытка | | `A` | `B`, target `B` уже пытался | плашка | плашка, auto запрещён | Mismatch симметричен. Текст не утверждает, какая сторона новее, и предлагает завершить restart HA, затем reload страницы. ### 11.2 Плашка - Компактная overlay-плашка располагается в card-level overlay над сценой и не изменяет высоту editor/header или fit viewport. Она остаётся доступна и в ранних full-card состояниях без готовой сцены (fixed-floor pending/invalid, пустая модель), если успешный `config/get` уже подтвердил mismatch; отдельные копии разметки в render-ветках не становятся независимыми состояниями. - Содержит понятный direction-neutral текст, версии frontend/backend и кнопку «Перезагрузить страницу» / `Reload page`. - Кнопка вызывает `window.location.reload()` только из trusted click/tap. - Плашка доступна с клавиатуры, имеет `role=status` или эквивалентный live-region без повторного объявления на каждый render; touch-target кнопки не менее 44×44 CSS px. - Она не перехватывает pan/zoom вне своей поверхности, не закрывает основной navigation и не меняет focus самопроизвольно. - В обычном режиме отображается при любом известном mismatch, включая editor и открытый dialog; reload остаётся осознанным действием пользователя. - Известный mismatch не блокирует вход в редактор. Существующий recovery #353 для lazy editor/onboarding runtime остаётся независимой проверкой fingerprint, но его terminal-тост с тем же советом о reload подавляется, пока уже видна version-mismatch плашка. Network/non-terminal toast с советом повторить действие сохраняется; terminal toast также сохраняется, если mismatch неизвестен и плашки нет. Одновременно два сообщения с одной просьбой о reload не показываются. - В kiosk плашка до первой разрешённой auto-попытки не показывается: это осознанное исключение из обычного mismatch UX ради тихого обновления настенной панели. Если safe state долго не наступает, карточка сохраняет текущий кадр; после первой попытки и сохранившегося mismatch появляется обычная плашка. - Motion — короткое opacity-появление/исчезновение; при `prefers-reduced-motion: reduce` без анимации. ### 11.3 Ровно одна тихая kiosk-перезагрузка Attempt хранится в `sessionStorage` под namespaced key и содержит целевую `integration_version`. Один module-level helper разделяет его между всеми full card на странице. 1. Перед `location.reload()` target синхронно записывается в `sessionStorage`. 2. Та же target после reload, при другом `CARD_VERSION` или в другом card instance не получает вторую auto-попытку. 3. Новая backend target может получить одну новую попытку. 4. Manual button не очищает guard и не обещает auto retry. 5. Если `sessionStorage` читать или писать нельзя, fail-safe — banner и никакой автоматической перезагрузки. 6. Совпадение версий не очищает сохранённую target: иначе reverse proxy, чередующий старый и новый frontend при том же backend, снова разрешит reload. Хранится одна небольшая строка без плана, entity ID и других данных. Auto-reload разрешён только при одновременном выполнении: - `config.kiosk === true` и компонент connected; - initial server load завершён, есть полный settled frame, нет recovery/loading; - режим `view`, `_editing === false`; - ни один принадлежащий карточке first-class dialog/confirm/menu overlay не открыт; единый predicate обязан включать как минимум editor-secondary dialogs, room/partition delete, import, backdrop guard, danger confirm и незавершённый `_vacFit`. Нативный HA more-info не включается по ненадёжному stale-полю: отдельного достоверного сигнала его закрытия у карточки нет. Вместо этого единая точка открытия more-info обязана продлить `_cyclePausedUntil` и для pointer, и для keyboard, и для внутренних программных путей; одной паузы из stage pointerdown недостаточно; - `_pendingPhysicalWrites.size === 0`, `_writesPending === 0` и нет незавершённой config write chain; - нет pending layout debounce/отправки и грязных несохранённых позиций устройств; - нет активного pointer/pinch/swipe/drag gesture или mode transition; - `Date.now() >= _cyclePausedUntil`; - `_zoom <= 1.001`. Проверка идёт независимым bounded controller/timer: `cycle: 0` не отключает механизм. Одновременно существует не более одного timer на card; disconnect, совпадение версий и смена target его отменяют. Проверка не чаще одного раза за animation frame/разумный timer и не добавляется в HA state render hot path. ## 12. Документация установки `README.md`, `README.ru.md`, `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md` содержат одну и ту же развилку: ### Storage mode (HA default) Обычно ручная запись не нужна. Если автоматическое подключение не сработало: Settings → Dashboards → меню ⋮ → Resources → Add resource; URL `/houseplan_files/houseplan-card.js`, type JavaScript module. После установки или изменения ресурса полностью reload страницы; hard reload shortcuts названы. ### YAML resources mode в HA 2026.2+ Современный канонический snippet управляет только источником ресурсов и оставляет сами dashboards в выбранном пользователем storage/YAML режиме. Его синтаксис подтверждён Home Assistant Core [#161816](https://github.com/home-assistant/core/pull/161816), исходником [HA 2026.2.0](https://github.com/home-assistant/core/blob/2026.2.0/homeassistant/components/lovelace/__init__.py) и [официальной документацией dashboards](https://www.home-assistant.io/dashboards/dashboards/): ```yaml lovelace: resource_mode: yaml resources: - url: /houseplan_files/houseplan-card.js type: module ``` ### Legacy HA 2024.6–2026.1 Для уже YAML-managed dashboard показан отдельно помеченный legacy snippet: ```yaml lovelace: mode: yaml resources: - url: /houseplan_files/houseplan-card.js type: module ``` Документация явно предупреждает, что `mode: yaml` меняет режим самого dashboard. Пользователь старого HA со storage dashboard должен применять UI Resources из предыдущего раздела, а не переключать dashboard ради карточки. Нельзя утверждать, что `lovelace.resources` всегда игнорируется storage-managed dashboard: в HA 2026.2+ именно `resource_mode: yaml` поддерживает YAML-ресурсы независимо от режима dashboard. UI Resources, в свою очередь, не заменяет YAML declaration, когда выбран YAML resource mode. Путь `/custom_components/...` остаётся явно запрещён. ## 13. Модель данных, compatibility и миграция - Формат плана, layout, storage schema и card config не меняются. - Websocket-протокол не меняется: `integration_version` остаётся существующим ответным compatibility-полем, новых request/response полей нет. - Служебный boolean-флаг показа первого уведомления в config entry читается как optional; отсутствие/невалидное значение означает «ещё не создавалось». Schema migration и перепись планов не нужны. - `sessionStorage` — transient browser-tab metadata, не экспортируется и не синхронизируется. - Downgrade безопасен: старый backend без `integration_version` даёт `unknown`, backend с иной валидной версией даёт symmetric mismatch. Запрос остаётся тем же и не требует compatibility retry. ## 14. i18n Новые строки добавляются одновременно: - frontend EN/RU/DE/FR: заголовок/текст version mismatch, подписи обеих версий, кнопка reload; - backend `strings.json` + EN/RU/DE/FR: текст одноразового notification и ключи `system_health.info.*`; - документация EN/RU синхронна по смыслу. Тексты не говорят «backend новее» и не обещают, что один reload всегда исправит незавершённое обновление. Значения status enum в System Health не требуют перевода для машинной диагностики, но их названия полей локализованы. ## 15. Критерии приёмки ### AC1 — документация режима (`unit`/docs contract) Оба README и оба User Guide содержат отдельную storage UI-инструкцию, современный HA 2026.2+ snippet с `lovelace.resource_mode: yaml`, явно помеченный legacy 2024.6–2026.1 snippet с `lovelace.mode: yaml` только для full-YAML dashboard и hard reload shortcuts. Возврат плоского top-level `resources:`, современного `mode: yaml` вместо `resource_mode: yaml` или совет переключить storage dashboard в legacy YAML ради карточки делает docs contract красным. ### AC2 — честный outcome регистрации (`backend`) Backend различает `created`, `updated`, `existing`, `registry_pending`, `transient_error`, `yaml_fallback`, `error_fallback`; существующая запись обновляется без дубля, а отсутствующий файл не сообщает успешный loader. ### AC3 — lifecycle retry без двойного loader (`backend` + mutation) Registry отсутствует на первой попытке и появляется после HA started: один retry через фиксированный секундный lifecycle delay регистрирует канонический resource и снимает fallback либо честно маркирует его остаток. Уже running HA также не делает retry в том же tick. Unload до event, во время delay и во время task отменяет callback/side effects; повторный setup не накапливает listeners. Мутанты «удалить retry», «убрать delay при already-running» и «не привязать callback/task к unload» краснеют. ### AC4 — одноразовое notification (`backend` + mutation) Первая доступная регистрация frontend создаёт локализованное persistent notification и сохраняет boolean-флаг. Повторный setup, retry completion и новая версия его не создают; missing file/static failure не расходуют право на показ; uninstall dismiss-ит его. Мутант «не сохранять флаг» краснеет повторным setup. ### AC5 — System Health (`backend`) Матрица missing file / registry success / pending→success / YAML fallback / exception возвращает правдивые поля §10, точный versioned URL и безопасную ошибку. Существующая статистика плана не исчезает. ### AC6 — точное frontend-сравнение (`unit` + mutation) Pure controller покрывает equal, symmetric mismatch, unknown и смену target. Успешный ответ без корректной `integration_version` очищает stale значение. В обычном режиме banner существует iff известен mismatch; kiosk-исключение до первой auto-попытки соответствует матрице §11.1. Отключение сравнения или принятие unknown за mismatch ловится unit/mutation gate. ### AC7 — обычный режим никогда не reload сам (`unit` + browser + mutation) При mismatch full card показывает плашку, но любое ожидание без click оставляет `location.reload` невызванным. Trusted button вызывает один reload. Мутант, разрешающий timer reload при `kiosk !== true`, краснеет. ### AC8 — kiosk safety (`unit` + browser + mutation) Каждый отдельный unsafe guard из §11.3 блокирует auto-reload; после перехода в полностью safe state target записывается до ровно одного reload. Минимум мутанты «игнорировать pause», «игнорировать dialog/editor», «игнорировать pending write» и «помечать attempt после reload» детерминированно краснеют. Открытие native HA more-info мышью/тапом, клавиатурой и внутренней карточкой продлевает ту же pause, поэтому ни один из этих путей не допускает немедленный kiosk reload. ### AC9 — защита от reload-loop (`unit` + browser + mutation) После simulated reload тот же backend target показывает banner и не вызывает reload повторно; это сохраняется при чередовании разных frontend-версий и во втором card instance той же вкладки. Новая backend target получает одну попытку. Исключение storage переводит в manual-only. Мутант «игнорировать сохранённую target» краснеет. ### AC10 — граница full/space UX (`unit`/browser) Runtime banner/auto-reload рендерит только full card. Space card продолжает штатно загружать тот же bundle и config, но не получает скрытого timer или нового UI этой задачи. ### AC11 — downgrade и lifecycle compatibility (`unit` + backend) Старый backend без `integration_version` даёт `unknown`; валидная отличающаяся версия не скрывается. Setup/unload/remove, старый websocket request и существующие `test_ha_setup.py` остаются зелёными. ### AC12 — View/touch/a11y и визуальная стабильность (`browser` + golden) Плашка не меняет stage bounds/fit, доступна клавиатурой и touch, не блокирует жесты сцены вне себя, не двигает focus. Desktop, узкий touch viewport и kiosk after-failed-attempt имеют принятый screenshot/golden; reduced-motion вариант не анимируется. При видимой version-mismatch плашке terminal fingerprint failure lazy runtime не добавляет второй toast с просьбой reload; non-terminal failure и terminal failure без плашки продолжают показывать соответствующий toast. ### AC13 — сборка и синхронные артефакты (`build` + ревью кода) Typecheck/unit/backend/build/bundle parity/no-new-any/check-docs и целевые smoke зелёные; EN/RU/DE/FR ключи полны; оба changelog и пользовательская документация обновлены. Три runtime-копии bundle синхронны; две committed trees (`dist` и integration frontend) побайтно совпадают. ## 16. План тестов и отрицательные доказательства ### Backend - расширить `tests_backend/test_ha_setup.py` матрицей outcomes, delayed registry, listener cleanup, idempotent reload, missing bundle и uninstall cleanup; - отдельные тесты `system_health_info` для §10; - one-time notification: first available setup, repeated setup/version, missing file/static failure и uninstall; - mutation witnesses для retry, unload-bound callback и persisted notice flag. ### Frontend unit - pure mismatch/session guard controller без DOM; - exact backend target, чередование frontend versions, storage exception, multiple instances и mark-before-reload; - каждый safe predicate независимо false/true; - authoritative очистка stale `integration_version` до unknown. ### Browser smoke/golden - full card mismatch в обычном View: banner, стабильный stage bbox, no auto, click reload spy; - попытка открыть editor при известном mismatch: terminal fingerprint failure не дублирует видимую плашку toast-ом; non-terminal failure остаётся видимым; без version banner terminal toast #353 сохраняется; - kiosk mismatch: unsafe→safe без предварительной плашки, one auto; remount/reload с той же backend target — banner и zero auto; - editor/dialog/pending-write/zoom/recent interaction отдельными probes; - native HA more-info через pointer, keyboard и внутренний программный путь одинаково продлевает interaction pause без reliance на stale open-marker; - narrow touch + keyboard focus + reduced motion; - space card загружается без runtime banner/timer. ### Docs/mutation - статический parser проверяет вложенность `resources`, современный `resource_mode: yaml`, отдельно версионированный legacy `mode: yaml`, запрет рекомендовать legacy mode для storage dashboard и отдельную UI-инструкцию; - `scripts/mutation-gate.mjs` хранит дорогие backend/browser mutants из AC3, AC4, AC7–AC9 с точными target test commands; - в документе code review каждый защитный AC получает строку «чем краснеет» по §2.7 `PROCESS.md`. ## 17. Затронутые файлы и модули Ожидаемый минимум: - `custom_components/houseplan/__init__.py`, `manifest.json`, `system_health.py` и runtime/store typing; - `custom_components/houseplan/strings.json` и translations EN/RU/DE/FR; - `src/houseplan-card.ts`, небольшой pure version controller, frontend i18n EN/RU/DE/FR и CSS; - `README.md`, `README.ru.md`, `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`, `docs/ARCHITECTURE.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, оба changelog; - backend/unit/browser/docs/mutation tests и синхронные generated bundles. Точные helper names и разбиение pure-модулей остаются за реализацией; границы поведения и доказательства менять нельзя без актуализации ТЗ. ## 18. Производительность и security - Registry retry — максимум один callback на entry setup; polling отсутствует. - Version comparison — O(1) только после config response/update, не на каждый HA state tick и не внутри geometry/render hot path. - Kiosk использует один bounded timer только пока существует непредпринятый mismatch; после match/attempt/disconnect timer уничтожается. - Banner добавляет постоянный DOM только во время mismatch. Geometry/Glow/device pipelines не меняются; новый performance capture не нужен, но штатные budget gates обязательны. - В System Health не попадают traceback, секреты, внешние filesystem paths или пользовательская конфигурация. - Одноразовый notice-флаг не меняет план или permission boundary; resource registration/removal остаётся серверной операцией. ## 19. Риски 1. **Двойное исполнение bundle:** fallback и resource могут сосуществовать после retry. Закрывается единым exact URL, удалением fallback и AC3. 2. **Повторяющееся notification:** закрывается persisted boolean и AC4. 3. **Потерянное первое notification:** флаг пишется только после доступного frontend/static path и успешного создания. 4. **Reload-loop kiosk:** sessionStorage backend target до reload и AC8/AC9. 5. **Потеря правки:** полный safe predicate, обычный режим manual-only. 6. **Frontend новее backend:** direction-neutral copy и symmetric matrix. 7. **Old backend не отдаёт version:** authoritative `unknown`, без ложного banner/reload. 8. **Несколько cards/tabs:** attempt общий в пределах вкладки, но не между вкладками; это намеренная граница `sessionStorage`. 9. **HA startup race/uninstall:** after dependency, one-shot lifecycle callback, unload cancellation и removal tests. 10. **Заблуждающий System Health:** отдельные file/static/loader/outcome/error поля, без обещания browser state. 11. **Дублирующиеся просьбы reload:** terminal toast #353 подавляется только при уже видимой version-mismatch плашке; остальные ошибки lazy runtime не скрываются, что проверяет AC12. ## 20. Rollback Откат — один revert продуктового коммита и синхронных тестов/документации/ bundles. План и layout не мигрируют. Старый backend игнорирует служебный boolean в config entry; он не требует ручного вмешательства. После отката вернутся невидимый fallback и необходимость ручного reload; зарегистрированный Lovelace resource с прежним корректным URL останется работоспособным. Удаление интеграции продолжает убирать ресурс. ## 21. Release-артефакты Поскольку исправление пользовательски видимо: - `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` получают короткий пункт со ссылкой на #462; - README и User Guide EN/RU меняются по §12; `docs/TESTING.md` больше не утверждает старый reload-контракт; - `docs/ARCHITECTURE.md` фиксирует authority resource loader, notification и full-card version controller; - `docs/DEVELOPMENT.md` фиксирует backend/frontend version test seam и lifecycle retry; - принимаются три новых product golden из AC12. Отдельно изменение `src/**` требует актуального Linux `Docs screenshots` artifact для всех десяти документационных кадров и fingerprint acceptance; неизменившиеся пиксели не переснимаются вручную; - release performance/security отдельных отчётов не требует; штатные gates и pre-beta runbook остаются обязательными; - generated bundle trees обновляются только через штатный build. ## 22. Принятые технические предположения — можно менять на ревью 1. Выбран `persistent_notification`, потому что контракт владельца — одна информационная подсказка после первой регистрации, а не сохраняемая неисправность Repairs. 2. Boolean «notification уже создавалось» хранится в `entry.data`, а не в плане или layout; точное имя поля внутреннее. 3. One-shot retry использует `homeassistant.helpers.start.async_at_started`, а после running-state — cancellable delay через штатный event/loop helper; listener, timer, task и поздние side effects привязаны к unload. 4. Loader outcome может быть dataclass/enum/typed dict; публичны только значения System Health. 5. Banner в kiosk появляется только после уже предпринятой auto-попытки для этой backend target; до неё сохраняется тихий текущий кадр, как решил владелец. 6. Одна сохранённая backend target достаточна: новая target заменяет старую, а смена frontend при неизменной target не сбрасывает guard. 7. Проверка всех dialog/gesture состояний может быть выделена в общий pure snapshot helper; перечисление §11.3 является обязательным контрактом. 8. Минимальный touch-target 44×44 CSS px выбран как локальный accessibility-порог для этой плашки; общего числового порога в канонических UX-документах проекта пока нет.