From 84f9901ca0611db8621193250776bba408595389 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sat, 15 Aug 2026 10:26:19 +0300 Subject: [PATCH] docs: specify HA area marker relocation Issue: #126 User-Visible: no --- docs/specs/126-ha-area-marker-relocation.md | 312 ++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 313 insertions(+) create mode 100644 docs/specs/126-ha-area-marker-relocation.md diff --git a/docs/specs/126-ha-area-marker-relocation.md b/docs/specs/126-ha-area-marker-relocation.md new file mode 100644 index 00000000..ec230e90 --- /dev/null +++ b/docs/specs/126-ha-area-marker-relocation.md @@ -0,0 +1,312 @@ +# Issue #126 — HA Area переносит marker в новую комнату + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/126 +- **Статус документа:** готово к будущей реализации; issue остаётся на `S3-spec` +- **Приоритет:** P2 +- **Тип:** bug, обычный трек +- **Пользовательское изменение:** да + +## 1. Сценарий + +Администратор переносит устройство или standalone entity в другую Area средствами +Home Assistant. House Plan уже открыт либо загружается позже. Если принадлежность +marker не переопределена в House Plan, план должен сам показать объект в комнате, +связанной с новой Area, и обратить внимание администратора красной меткой. + +## 2. Что человек увидит до и после + +До изменения сохранённая позиция оставляет marker в прежней комнате при смене HA +Area внутри одного пространства; после изменения marker появляется в автосетке +новой комнаты с обычной красной меткой «требует внимания». + +## 3. Проблема + +Layout keyed по marker id и space, но не хранит provenance комнаты/Area. +`_livePos()` принимает сохранённые координаты, пока `saved.s === d.space`, а +`_syncNewDevices()` сравнивает только множество id. Поэтому изменение Area внутри +того же space не инвалидирует position и не добавляет `new_device_ids`. + +Это нарушает J6: план продолжает показывать устройство там, где его больше нет, +и не сообщает администратору о расхождении. + +## 4. Решения владельца + +1. Сохранённая после drag позиция — раскладка, а не принадлежность; смена HA Area + её перебивает. +2. Явные `marker.area` и `marker.room_id` всегда побеждают registry Area. +3. Новое место выбирает обычная автосетка комнаты, не строгий центр. +4. Переезд использует существующий красный badge `new_device_ids`, без нового + вида состояния. +5. Охвачены device и entity markers, если их effective room получена напрямую + из HA registry. Composite light groups с Area, выведенной из одного участника, + автоматически не переносятся. + +## 5. Scope + +- обнаружение смены authoritative registry Area для eligible marker; +- invalidation сохранённого layout даже в том же space; +- автоматическая позиция в новой комнате через canonical `defaultPositions`; +- existing red new-device attention и его обычный acknowledgement; +- live registry refresh, cold start, multi-client config/layout events; +- migration/backfill для уже существующих layout без Area provenance; +- device/entity binding, explicit override и composite-group exclusions; +- frontend/backend schema validation, unit/backend/smoke coverage и docs. + +## 6. Не входит в задачу + +- управление Area в HA из House Plan; +- перенос Area самой комнаты или изменение room↔Area binding; +- новый badge/history/notification/toast для переезда; +- Undo/Redo внешнего registry change; +- автоматический перенос composite/folded light groups; +- сохранение старой ручной позиции как recoverable history; +- переразмещение при каждом registry rebuild без фактической смены Area; +- lifecycle устройств без Area либо Area, не привязанной к комнате — это #29. + +## 7. Источник принадлежности и eligibility + +Для каждого renderable marker вычисляется `PlacementAuthority`: + +1. `marker.room_id` — explicit House Plan room, auto-relocation запрещена; +2. `marker.area` — explicit House Plan Area, auto-relocation запрещена; +3. direct registry Area device binding — eligible; +4. direct registry Area standalone entity binding — eligible; +5. Area, выведенная из member composite/group/fold — ineligible; +6. virtual/no authoritative Area — ineligible. + +Один pure resolver используется discovery, placement и relocation. Нельзя +определять eligibility по id prefix, icon или текущим координатам. + +Registry snapshot считается authoritative только после полного registry load. +Limited-permission/unverified кадр не считается сменой Area и не удаляет position. + +## 8. Persisted provenance + +Чтобы отличить новую Area от reload с уже сохранённой позицией, config settings +получают optional internal map: + +```ts +settings.marker_area_snapshot?: Record +``` + +Имя может быть уточнено ревьюером, но semantics фиксированы: + +- значение — последняя успешно обработанная direct registry Area; +- map bounded множеством известных eligible marker ids; +- tombstone/removed/missing ids очищаются вместе с действующей lifecycle hygiene; +- поле не является пользовательской настройкой и не отображается в UI; +- unknown sibling keys сохраняются по compatibility policy; +- backend validator ограничивает key/value strings и общий config-size budget. + +Layout v2 (`{s,x,y}`) не расширяется: принадлежность — discovery/config lifecycle, +а не геометрия координаты. Это исключает divergent Area metadata в каждой +position write. + +## 9. Обнаружение и обработка смены Area + +После authoritative registry rebuild для eligible marker: + +1. получить previous area из snapshot; +2. получить current direct registry area; +3. разрешить current area ровно в одну room текущего model; +4. если previous=current — no-op; +5. если target room отсутствует/неоднозначна — отложить, snapshot не продвигать; +6. если area изменилась — удалить saved layout marker id; +7. пересчитать общую автосетку target room; +8. добавить marker id в existing `new_device_ids`; +9. только после успешной layout invalidation записать snapshot=current. + +Порядок fail-safe: layout delete раньше config snapshot. Если config write затем +падает, повторная обработка идемпотентно удалит уже отсутствующий layout и снова +попытается записать attention. Нельзя сначала признать Area обработанной, а потом +оставить старую позицию при ошибке layout store. + +Локальные `_layout`, `_defPos`, dirty/sent position state и render snapshot +обновляются атомарно для кадра; промежуточный marker в старой комнате после +authoritative event не показывается. + +## 10. Initial backfill старых конфигов + +При отсутствии snapshot для eligible marker: + +- если saved layout отсутствует — записать current Area без attention/move; +- если saved point лежит внутри комнаты с той же Area — сохранить position и + записать current Area; +- если saved point однозначно лежит внутри другой room с другой Area, считать это + уже существующим stale same-space case: удалить position, автопоставить в + current room и добавить `new_device_ids`; +- если point вне всех комнат, на границе нескольких комнат либо geometry + невалидна — сохранить position и записать current Area; небезопасную историю + по координате не угадывать; +- explicit override и composite group в backfill не участвуют. + +Point-in-room использует canonical final room polygons в plan coordinates, без +decor/device bounds. Backfill выполняется один раз и не превращает обычный reload +в постоянное auto-layout. + +## 11. Автосетка и attention + +Удаление saved position возвращает marker в тот же `defaultPositions()` path, +который уже используется для нового устройства и переезда между пространствами. +Сетка учитывает другие markers комнаты и выдаёт normalized finite position. + +`new_device_ids` переиспользуется дословно: + +- badge, цвет, фильтрация и acknowledgement остаются прежними; +- повторные registry events не добавляют duplicate id; +- acknowledgement не возвращает старую position и не меняет snapshot; +- несколько переехавших markers раскладываются одним детерминированным pass, а + не все в центр. + +External relocation не попадает в position-only Undo stack #74. Это изменение +источника истины, а не команда редактора. + +## 12. Multi-client и races + +- Первый клиент, успешно обработавший event, публикует layout/config revisions; + остальные принимают их обычным subscription path. +- Concurrent drag со старой layout revision не должен воскресить position после + relocation; existing expected revision/conflict resync остаётся authority. +- Если пользователь в House Plan успел сохранить explicit room override до + commit relocation, свежий config reload отменяет auto move. +- Registry event без изменившегося fingerprint не создаёт writes. +- Ошибка backend оставляет диагностируемый retryable state без production loop; + snapshot не продвигается мимо неуспешного layout delete. + +## 13. UX, touch и accessibility + +Новое управление не добавляется. В View/Device editor marker визуально появляется +в новой room; красная метка доступна тем же mouse/touch/keyboard paths, что у +нового устройства. На touch View это release-blocking display correctness. + +Перемещение не переносит DOM focus на marker и не открывает dialog. Если dialog +этого marker открыт во время registry change, он закрывается либо refresh-ится по +действующему stale-binding contract; сохранить старую room через stale draft +нельзя. + +## 14. Модель данных, migration и compatibility + +- Добавляется только optional internal snapshot map §8. +- Backend Store version не повышается, если optional field валидируется без + destructive rewrite; отсутствие поля запускает frontend backfill §10. +- Поле регистрируется в `docs/CONFIG-COMPATIBILITY.md` и + `scripts/config-field-registry.mjs` с owner, read/write/cleanup policy. +- Export/import сохраняет snapshot только как internal lifecycle metadata по + текущей full-config policy; restore на другом HA не может автоматически + перемещать marker до authoritative registry comparison. +- Unknown/legacy values fail safe: invalid entry игнорируется для конкретного id, + но не стирает layout до §10 inference. + +## 15. i18n + +Новых строк нет: используется существующая красная метка и текущие названия +комнат/devices. Если реализация добавляет toast или отдельную подпись «переехал», +это выходит за принятое решение и требует отдельного product scope. + +## 16. Acceptance criteria + +1. **AC1 — same-space relocation.** Direct-registry device с saved layout после + Area A→B в том же space теряет saved position, попадает в автосетку room B и + получает existing red badge. **Доказательство:** unit + browser smoke. +2. **AC2 — cross-space parity.** Тот же outcome действует при переходе между + spaces и не регрессирует уже работающий case. **Доказательство:** unit/smoke. +3. **AC3 — explicit wins.** `marker.room_id`/`marker.area` сохраняют room и saved + position при HA Area change. **Доказательство:** resolver unit matrix. +4. **AC4 — binding scope.** Direct device/entity markers переносятся; composite/ + inferred groups и virtual markers — нет. **Доказательство:** unit matrix. +5. **AC5 — backfill.** Старый saved point в другой однозначной room исправляется; + same-room/outside/ambiguous point безопасно сохраняется. **Доказательство:** + migration unit + cold-start smoke. +6. **AC6 — attention lifecycle.** Existing `new_device_ids` badge ставится один + раз и очищается обычным acknowledgement без нового state kind. + **Доказательство:** logic unit + browser smoke. +7. **AC7 — fail-safe writes.** Config snapshot не продвигается при failed layout + delete; retry и concurrent revision не воскрешают stale position. + **Доказательство:** backend/frontend failure tests. +8. **AC8 — no Undo.** External relocation не добавляется в #74 stack. + **Доказательство:** command-stack unit/source contract. +9. **AC9 — bounded/performance.** No-change registry rebuild даёт zero writes и + O(markers+rooms) bounded pass. **Доказательство:** counter unit + performance smoke. +10. **AC10 — compatibility.** Старые config/layout читаются, invalid snapshot + fail-safe, limits/exports documented. **Доказательство:** backend validation, + round-trip tests и registry audit. + +## 17. План автотестов + +### Unit + +- placement authority device/entity/explicit/composite/virtual matrix; +- same-space/cross-space/no-change/unbound target transitions; +- deterministic multi-marker autogrid; +- backfill point in same/other/outside/boundary/degenerate room; +- red badge add/dedupe/acknowledge; +- no command-stack entry and no write on identical registry fingerprint; +- failure ordering and stale revision recovery. + +### Backend + +- optional snapshot validation, limits and unknown fields; +- config round-trip/export/import; +- layout delete revision event and conflict behavior; +- native Windows pure subset may run, full HA harness authority is Linux CI/WSL. + +### Browser smoke + +- live HA registry Area change within one space with saved drag position; +- same transition for standalone entity marker; +- explicit room override, composite group exclusion; +- cold start from old config stale point; +- two devices moving together use separate autogrid slots; +- two-card/multi-client revision event; +- View/touch red badge and dialog open during transition. + +### Golden/performance + +- before/after fixture with marker and red badge in target room; +- no unrelated visual baseline acceptance; +- large registry no-change and relocation counters; no long task or repeated write. + +## 18. Затронутые поверхности + +- registry/device rebuild and placement lifecycle in `src/houseplan-card.ts`; +- pure placement/new-device helpers in `src/logic.ts`, `src/space-geometry.ts` or + focused new module; +- config/backend validation and compatibility registry; +- layout/config event tests, browser/golden/performance fixtures; +- `docs/ARCHITECTURE.md`, `docs/FILTERING.md`, + `docs/CONFIG-COMPATIBILITY.md`, `docs/USER-GUIDE.ru.md`, `docs/TESTING.md`. + +## 19. Риски и откат + +| Риск | Мера | +| --- | --- | +| Старый manual layout ошибочно принят за stale | backfill только при однозначной другой room | +| Config говорит processed до layout | delete-first fail-safe ordering | +| Group прыгает по Area участника | direct-provenance eligibility | +| Multi-client возвращает старую позицию | revision conflict/resync | +| Registry временно пуст | authoritative-frame guard | + +Откат прекращает чтение/запись snapshot map и возвращает прежнюю priority saved +layout. Optional metadata можно оставить ignored; destructive rollback не нужен. + +## 20. Release-артефакты + +Implementation commit имеет `User-Visible: yes` и одновременно обновляет: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`; +- `docs/USER-GUIDE.ru.md` — registry Area, explicit room override и red badge; +- `docs/ARCHITECTURE.md`, `docs/FILTERING.md`, + `docs/CONFIG-COMPATIBILITY.md`; +- `docs/TESTING.md`. + +Нужны targeted browser/golden artifacts, exact backend/unit commands и Linux CI +HA-harness evidence. + +## 21. Принятые технические предположения + +- Area provenance хранится bounded internal config map, не в layout entry; +- old-layout repair использует canonical point-in-room только для однозначного + stale case; +- layout delete выполняется раньше snapshot commit; +- unbound/ambiguous Area откладывает обработку и не продвигает snapshot; +- точное имя map/helper можно изменить на ревью без изменения semantics. diff --git a/docs/specs/README.md b/docs/specs/README.md index 5e01ac54..e2d399cb 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -45,6 +45,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#89](https://github.com/Matysh/houseplan-card/issues/89) Опциональный объёмный 2.5D/изометрический вид | [089-isometric-view.md](089-isometric-view.md) | | [#89](https://github.com/Matysh/houseplan-card/issues/89) Этап 1: объёмный вид за флагом Labs | [089-isometric-view-stage1.md](089-isometric-view-stage1.md) | | [#98](https://github.com/Matysh/houseplan-card/issues/98) Единая система пульсаций и активностей устройства | [098-device-pulse-system.md](098-device-pulse-system.md) | +| [#126](https://github.com/Matysh/houseplan-card/issues/126) HA Area переносит marker в новую комнату | [126-ha-area-marker-relocation.md](126-ha-area-marker-relocation.md) | | [#131](https://github.com/Matysh/houseplan-card/issues/131) Полный первый кадр View у read-only-пользователя | [131-readonly-cold-start.md](131-readonly-cold-start.md) | | [#138](https://github.com/Matysh/houseplan-card/issues/138) Автозамыкание комнаты по существующей стене | [138-adjacent-room-autoclose.md](138-adjacent-room-autoclose.md) | | [#146](https://github.com/Matysh/houseplan-card/issues/146) Четырёхфазный фон «Следует за Солнцем» | [146-four-phase-sun-background.md](146-four-phase-sun-background.md) |