Files
houseplan-card/docs/specs/462-card-resource-registration.md
T
2026-09-05 16:00:54 +03:00

714 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #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=<VERSION>` исправен и не является причиной.
Полевой сценарий #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-документах проекта пока нет.