docs: specify summary recovery consistency (#490)

Issue: #490
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-09 00:10:16 +03:00
parent 24c1b723f9
commit fd364c57a8
2 changed files with 263 additions and 0 deletions
@@ -0,0 +1,262 @@
# #490 — Атомарный recovery и live-состояние сводной панели
- **Issue:** https://github.com/Matysh/houseplan-card/issues/490
- **Тип / приоритет:** bug / P1
- **Статус ТЗ:** готово к ревью
- **Трек:** полный; исправление пересекает сохранение общего конфига,
многоклиентный revision-контракт и высокочастотную HA-инвалидацию render
- **Оценка:** пользовательская ценность 9/10; ценность для разработки 9/10;
сложность 5/10; риск 8/10
- **Связано:** #437 (исходная сводная панель), #451 (render lifecycle),
#493 (отдельные acceptance-дефекты панели; не дубликат)
## 1. Пользовательский сценарий
Персона — администратор дома, который настраивает сводную панель одновременно
с другим открытым клиентом House Plan, либо смотрит на панели значение датчика,
не размещённого отдельной иконкой на плане. Поверхности — диалог настроек
сводной панели и сама панель в обычной dashboard card или sidebar panel. Момент
— потеря ответа после успешного сохранения либо очередное обновление HA state.
## 2. До и после
**До:** потерянный ответ `config/set` может выглядеть как успешное сохранение,
но вернуть в локальную память устаревший общий конфиг и затереть параллельную
правку при следующей записи; датчик, используемый только сводной панелью,
остаётся со старым значением до случайной полной перерисовки.
**После:** успешный lost-ACK recovery принимает целиком подтверждённый сервером
конфиг и его revision, поэтому чужие правки сохраняются; любое изменение
источника видимого значения сводной панели своевременно обновляет строку без
лишнего пересчёта геометрии плана.
## 3. Подтверждённые причины
1. `LoadedSummaryPanelRuntime.saveDialog()` после ошибки записи перечитывает
authoritative config и сравнивает сохранённый `settings.summary_panel` с
draft. При совпадении принимается только `rev`, после чего `_serverCfg`
безусловно заменяется старым `candidate`. Параллельная правка другой части
конфига теряется локально и попадёт в следующую полнодокументную запись.
2. Фильтр HA-обновлений принимает зависимости из
`RenderDeviceSnapshot.entityIds`. Источники `summary_panel.blocks[].values[]`
туда не входят, если тот же entity не используется устройством, комнатой,
проёмом или декором. Поэтому такой tick классифицируется как посторонний и
render сводной панели не запускается.
## 4. Скоуп
1. Сделать lost-ACK recovery сохранения панели атомарным по паре
`authoritative config + revision`.
2. Сохранить существующий optimistic revision guard и сериализацию записей.
3. Добавить entity-источники действующей конфигурации сводной панели в единый
render dependency projection карточки.
4. Обновлять значения панели при изменении, появлении, исчезновении и
unavailable/recovered-переходах её entity-источников.
5. Добавить чистые unit-проверки и интеграционный browser smoke, который
воспроизводит обе исходные регрессии через production runtime.
6. Добавить запись об исправлении в оба changelog.
## 5. Не входит
- новая настройка, новый формат панели или изменение её визуального макета;
- изменение backend-схемы, `config/set`, локальных preference keys или API v1;
- автоматическое слияние двух разных правок самой `summary_panel`;
- polling датчиков, отдельная подписка или обход всего `hass.states`;
- изменение поведения system-источников (`device_count`, `total_area`,
`datetime`) сверх текущих контрактов #437;
- исправления #493 и рефакторинг всех writers проекта.
## 6. Контракт сохранения и recovery
### 6.1 Обычный ответ
Если `config/set` завершился успешно, остаётся текущий контракт: в очередь
передаётся полный canonical candidate на ожидаемом revision, принимается
возвращённый revision, диалог закрывается, локальные preferences сохраняются.
### 6.2 Потерянный ACK после принятой записи
Если transport promise завершился ошибкой, runtime один раз вызывает
существующий authoritative `config/get` и проверяет одновременно:
1. ответ содержит валидный полный `ServerConfig`;
2. сохранённый `settings.summary_panel` семантически равен нормализованному
draft;
3. ответ содержит применимый authoritative revision.
При выполнении условий запись считается принятой сервером. Карточка обязана
атомарно принять **весь** authoritative config и его revision через общий
config-adoption seam. Stale `candidate` после этого не устанавливается. Его
fingerprint, cache snapshot, модели и зависимые runtime-проекции также не могут
остаться смешанными с серверным документом.
Это означает, что параллельная правка заголовка пространства, markers,
settings, decor или иной части документа сохраняется в памяти клиента. Любая
следующая запись строится от принятого документа и отправляет authoritative
revision как `expected_rev`.
### 6.3 Настоящий конфликт или неясный исход
Если panel payload на сервере отличается от draft, config отсутствует/невалиден,
revision отсутствует либо повторное чтение не удалось, исходная запись не
объявляется успешной. Диалог остаётся открыт и показывает существующую ошибку
или conflict-состояние. Runtime не собирает гибрид из candidate и ответа и не
перезаписывает локальную базу неподтверждёнными данными.
Если authoritative adoption обнаруживает новую структурную базу, сохраняются
действующие правила сброса stale history/drag/cache и continuity #73/#451.
Recovery панели не получает собственного обходного варианта этих правил.
## 7. Контракт live-источников
### 7.1 Проекция зависимостей
Чистый resolver возвращает уникальные непустые `entity_id` всех entity-values
из действующей поддерживаемой `settings.summary_panel` версии 1. Учитываются
все сохранённые blocks и values, включая временно скрытый block, другой scope и
локально выключенную панель: их число ограничено схемой, а стабильная полная
проекция не создаёт stale dependency при локальном toggle или смене этажа.
Derived default, unsupported future schema и system-values не добавляют entity
dependencies. Дубликаты схлопываются. Resolver не читает HA и не мутирует
конфиг.
Результат передаётся в существующий `entityIds` render snapshot наряду с
источниками устройств, комнат, проёмов и декора. Отдельного listener, timer или
второго snapshot не создаётся.
### 7.2 Инвалидация и отображение
При смене identity state row любого такого entity обычный HA assignment
классифицируется как relevant state update. Следующий согласованный render
показывает:
- новое отформатированное значение и единицу;
- текущий `unavailable` по прежнему правилу #437;
- восстановленное значение после unavailable;
- placeholder отсутствующего источника после удаления state row и значение
после его повторного появления.
Панель читает тот же immutable HA frame, что и остальной план. Изменение entity,
которого нет в общей dependency projection, остаётся `none` и не запускает
полный render. Relevant state-only tick не меняет config/layout/model identity,
не увеличивает geometry epochs и не строит заново wall/room/isometric topology.
## 8. Совместимость, данные и i18n
- Схема `ServerConfig`, storage и API не меняются; миграции нет.
- Старые конфиги и backend без `summary_panel_api` сохраняют текущее fallback-
поведение.
- Новых строк нет. EN/RU/DE/FR dictionaries не меняются.
- Разметка, focus, keyboard/touch и права доступа не меняются.
## 9. Критерии приёмки
### AC1 — атомарный lost-ACK recovery
После принятой сервером записи с потерянным ответом и одновременной правки
несвязанного поля другим клиентом диалог закрывается как успешный, а локальный
config и revision полностью равны authoritative ответу.
**Доказательство:** unit/integration test runtime с fake transport и browser
smoke на production bundle.
### AC2 — следующая запись ничего не затирает
Следующая обычная запись строится от принятого authoritative документа,
посылает его revision в `expected_rev` и сохраняет параллельную несвязанную
правку.
**Доказательство:** последовательный lost-ACK → second write test с проверкой
payload и revisions.
### AC3 — настоящий конфликт остаётся конфликтом
Если сохранённая сервером панель не равна draft либо recovery read не даёт
валидную пару config/revision, stale candidate не принимается, диалог остаётся
открыт и пользователь получает существующую error/conflict обратную связь.
**Доказательство:** отрицательная матрица unit/integration tests.
### AC4 — summary-only entity живой
Entity, не используемый ни одним другим слоем плана, обновляет строку сводной
панели на переходах value → new value → unavailable → recovered → missing →
present без ручного открытия диалога, смены пространства или resize.
**Доказательство:** pure dependency unit + `demo/smoke_summary_panel.mjs` на
реальном компоненте и production bundle.
### AC5 — фильтр и geometry fast path сохранены
Tick постороннего entity не вызывает render. Tick summary-only source вызывает
не более одного согласованного render и не меняет счётчики/identity структурной
геометрии, config epoch и layout revision.
**Доказательство:** render-invalidation/lifecycle unit и smoke counters до/после.
### AC6 — совместимость поверхности
Существующие default/system rows, локальный Show toggle, responsive placement,
settings validation и все EN/RU/DE/FR строки проходят без изменений. Новых
runtime dependencies и сетевых запросов в steady state нет.
**Доказательство:** текущий summary unit/smoke, bundle manifest/budget и diff
audit.
## 10. План тестирования
1. `test/summary-panel.test.mjs`: resolver entity dependencies — duplicate,
hidden/other-scope, system, unsupported и empty cases.
2. Фокусный runtime unit либо вынесенный чистый adoption helper: success,
lost-ACK full adoption, mismatch, missing config/rev и failed GET.
3. `test/render-device-snapshot.test.mjs` и
`test/render-invalidation.test.mjs`: dependency попадает в snapshot и
relevant/irrelevant state identities классифицируются правильно.
4. `demo/smoke_summary_panel.mjs`: настоящий диалог/WS fake reproducer и
summary-only live transitions; assertions следующей записи и geometry
counters.
5. Implementation loop: `npm run typecheck`, `npm run test:unit`, `npm run build`.
6. Перед S7: именованный summary smoke и затронутые render smoke; golden/smoke/
performance full gates остаются каноном pre-release, а Linux HA harness — CI.
## 11. Риски и защита
| Риск | Защита |
|---|---|
| После recovery принимается только rev или только config | один host adoption seam и последовательный AC1/AC2 test |
| Настоящий конфликт ошибочно закрывает диалог | строгая equality панели плюс валидная полная пара config/rev |
| Новый source забывают добавить в fast filter | чистая projection-функция от canonical summary config |
| Любой HA tick снова начинает render | bounded dependency set и explicit unrelated-tick assertion |
| State tick перестраивает геометрию | counters/identity assertions на production smoke |
| Future schema случайно читается как v1 | использовать тот же поддерживаемый `summaryPanelOf` contract |
## 12. Rollback
Frontend-изменение откатывается одним commit без миграции данных. После rollback
конфиг и API остаются читаемыми, но возвращаются обе исходные регрессии. Не
допускается частичный rollback только dependency projection или только full
adoption, если тесты по-прежнему обещают общий контракт.
## 13. Release-артефакты
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: одна пользовательская запись о
сохранении параллельных правок и живых значениях сводной панели.
- `docs/USER-GUIDE.md` / `.ru.md`: не меняются — исправляется уже описанное
ожидаемое поведение, новых действий пользователя нет.
- Golden/screenshots не требуются: визуальный дизайн не меняется; browser smoke
проверяет текстовые состояния и отсутствие geometry churn.
- Bundle budget и smoke links обновляются только если добавляется новый smoke
или меняется lazy graph; новая production dependency запрещена.
## 14. Принятые предположения
1. Совпадение нормализованной `summary_panel` после transport error является
достаточным доказательством lost ACK только вместе с валидным полным config и
revision из последующего authoritative GET.
2. Полная сохранённая v1-конфигурация панели формирует зависимости независимо
от текущей локальной видимости; максимум 200 values делает набор bounded.
3. `unavailable` и missing отображаются по существующим правилам #437; задача
меняет своевременность обновления, а не текст placeholder.
4. Публичный UX, backend и формат данных не меняются, поэтому дополнительных
вопросов владельцу и `blocked` не требуется.
+1
View File
@@ -28,6 +28,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| Issue | ТЗ |
|---|---|
| [#490](https://github.com/Matysh/houseplan-card/issues/490) Атомарный recovery и live-состояние сводной панели | [490-summary-recovery-live-state.md](490-summary-recovery-live-state.md) |
| [#486](https://github.com/Matysh/houseplan-card/issues/486) Панель House Plan в боковом меню HA | [486-house-plan-panel.md](486-house-plan-panel.md) |
| [#489](https://github.com/Matysh/houseplan-card/issues/489) Объявленный `data-hp`-контракт для UI и E2E | [489-data-hp-contract.md](489-data-hp-contract.md) |
| [#484](https://github.com/Matysh/houseplan-card/issues/484) Внешняя размерная цепь ступенчатого фасада в PDF | [484-pdf-exterior-dimension-chain.md](484-pdf-exterior-dimension-chain.md) |