diff --git a/docs/specs/490-summary-recovery-live-state.md b/docs/specs/490-summary-recovery-live-state.md new file mode 100644 index 00000000..d4bfd4ea --- /dev/null +++ b/docs/specs/490-summary-recovery-live-state.md @@ -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` не требуется. diff --git a/docs/specs/README.md b/docs/specs/README.md index dc9d84cd..7711d6a9 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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) |