mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 06:38:57 +00:00
docs: specify empty-roster controller state
Issue: #318 User-Visible: no
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# Issue #318 — активный контроллер без собственных сущностей следует `controls`
|
||||
|
||||
- Дата: 2026-08-28
|
||||
- Тип: bug · приоритет P2
|
||||
- Оценка: пользовательская ценность 6/10 · сложность 3/10 · риск 4/10
|
||||
- Issue: [#318](https://github.com/Matysh/houseplan-card/issues/318)
|
||||
- Связанные контракты: [#251](https://github.com/Matysh/houseplan-card/issues/251),
|
||||
[#267](https://github.com/Matysh/houseplan-card/issues/267),
|
||||
[#274](https://github.com/Matysh/houseplan-card/issues/274)
|
||||
- Ветка: `issue/318-switch-render-state`
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`,
|
||||
`docs/DEVICE-PRESENTATION.md`, `docs/CONFIG-COMPATIBILITY.md`,
|
||||
`docs/TOUCH-SUPPORT.md`, `docs/USER-GUIDE.md`,
|
||||
`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`.
|
||||
|
||||
## 1. Сценарий и персона
|
||||
|
||||
Житель дома смотрит в View или kiosk на физический настенный выключатель,
|
||||
который управляет лампой либо группой через сохранённые `controls`. Home
|
||||
Assistant содержит активную запись устройства, но интеграция не создаёт для
|
||||
него ни одной собственной сущности. При переключении управляемого света
|
||||
человек ожидает увидеть обычный нейтральный выключатель для `off` и жёлтый —
|
||||
для `on`, а не постоянный признак недоступного устройства.
|
||||
|
||||
Поверхности: основной план в View/kiosk, hosted Static и живой preview редактора
|
||||
устройств. Это J1 «что происходит сейчас» и J3 «быстрое очевидное действие» из
|
||||
`docs/SCOPE.md`.
|
||||
|
||||
## 2. Что человек увидит до и после
|
||||
|
||||
**До:** выключатель постоянно выглядит недоступным независимо от того,
|
||||
включается или выключается управляемый свет.
|
||||
|
||||
**После:** при включённой цели выключатель становится жёлтым, при выключенной
|
||||
или недоступной цели — обычным нейтральным; приглушение сохраняется только там,
|
||||
где Home Assistant действительно предоставляет собственные сущности
|
||||
контроллера и все они не имеют живого состояния.
|
||||
|
||||
## 3. Проблема и подтверждённый диагноз
|
||||
|
||||
Полевой сценарий воспроизведён на `dev.houseplan.tech`. Сохранённый marker
|
||||
`Wall Switch Kitchen` имеет активный `device:` binding, ноль собственных строк
|
||||
entity registry и `controls: [light.wall_lights]`.
|
||||
|
||||
`resolvePresentationSources()` корректно берёт `working/neutral` у
|
||||
управляемой цели. Затем `resolveDevicePresentation()` классифицирует marker как
|
||||
controller face и вызывает `controllerAvailability()`. Функция ищет хотя бы
|
||||
одно живое состояние в `DevItem.entities`; пустой список безусловно даёт
|
||||
`unavailable`. Presentation policy заменяет только availability агрегата,
|
||||
поэтому target status обновляется внутри проекции, но класс `unavail` имеет
|
||||
визуальный приоритет и скрывает и жёлтую, и нейтральную подложку.
|
||||
|
||||
Обновление HA-состояний и repaint исправны: на приложенных кадрах одновременно
|
||||
меняются лампы и комнатный счётчик. #274 также уже гарантирует одинаковый roster
|
||||
и одну semantic generation для плана и preview. Дефект находится не в snapshot
|
||||
continuity, а в неразличении двух случаев: «у контроллера нет канала телеметрии»
|
||||
и «его существующие каналы сообщают unknown/unavailable».
|
||||
|
||||
## 4. Зафиксированное решение владельца
|
||||
|
||||
1. Активный физический `device:` binding с пустым собственным roster считается
|
||||
доступным: отсутствие телеметрии не является доказательством offline.
|
||||
2. Его `working/neutral` следует существующему resolved graph `controls`:
|
||||
доступная цель `on` даёт жёлтую подложку, все доступные цели `off` —
|
||||
нейтральную.
|
||||
3. Если roster непуст, но ни одна собственная сущность не имеет живого
|
||||
состояния, контроллер остаётся `unavailable` по контракту #251.
|
||||
4. Явные `ha_disabled`, `orphaned`, virtual controller, partial controls,
|
||||
безопасный no-op и приоритет alarm не меняются.
|
||||
5. Plan, preview и hosted Static используют один результат общего presentation
|
||||
resolver.
|
||||
|
||||
Продуктовых вопросов не осталось.
|
||||
|
||||
## 5. Скоуп
|
||||
|
||||
### Входит
|
||||
|
||||
- различение пустого и непустого собственного roster для активного
|
||||
физического device-binding;
|
||||
- доступная проекция пустого roster с target-derived `working/neutral`;
|
||||
- сохранение `unavailable` для непустого roster без живых состояний;
|
||||
- совпадение View, kiosk, hosted Static и device preview;
|
||||
- актуализация строки S05 таблицы решений #267 и RU/EN документации;
|
||||
- unit, production-bundle smoke и mutation evidence;
|
||||
- оба changelog.
|
||||
|
||||
### Не входит
|
||||
|
||||
- считать запись device registry доказательством online, когда у устройства
|
||||
есть собственные сущности;
|
||||
- менять entity-bound markers: активная `entity:` привязка содержит точную
|
||||
сущность и не образует пустой roster;
|
||||
- новый badge, warning glyph, pulse, цвет или текст причины;
|
||||
- изменение resolver управляемых целей, Toggle, confirmation или toast;
|
||||
- изменение Glow, room fill, light statistics или service payload;
|
||||
- исправление/миграция данных dev-стенда;
|
||||
- persisted config, backend, schema/model version или registry API;
|
||||
- расширение поведения на `unverified`, `ha_disabled` либо `orphaned` binding.
|
||||
|
||||
## 6. Контракт поведения
|
||||
|
||||
### 6.1. Термины
|
||||
|
||||
`own roster` — `DevItem.entities`, то есть активные собственные сущности exact
|
||||
binding после действующих registry-фильтров. `roster empty` означает длину 0, а
|
||||
не список из сущностей без live state.
|
||||
|
||||
`active physical device binding` выполняет одновременно:
|
||||
|
||||
- `bindingKind === 'device'` либо `marker.binding` начинается с `device:`;
|
||||
- `bindingStatus.kind === 'active'`;
|
||||
- marker не virtual.
|
||||
|
||||
Отсутствующий `bindingStatus`, `unverified`, `ha_disabled` и `orphaned` не дают
|
||||
права применять fallback. Техническая реализация вправе выразить этот
|
||||
предикат иначе, если наблюдаемый контракт остаётся тем же.
|
||||
|
||||
### 6.2. Матрица availability/status
|
||||
|
||||
При включённых live states и сохранённых внешних `controls`:
|
||||
|
||||
| Binding и собственный roster | Управляемые цели | Итоговая проекция |
|
||||
|---|---|---|
|
||||
| active device, roster пуст | хотя бы одна доступная `on` | available + working; жёлтая |
|
||||
| active device, roster пуст | все доступные `off` | available + neutral |
|
||||
| active device, roster пуст | все unavailable/missing/отфильтрованы tombstone | available + neutral |
|
||||
| active device, roster непуст, хотя бы одна own state живая | target `on` | available + working |
|
||||
| active device, roster непуст, хотя бы одна own state живая | targets `off`/unavailable | available + neutral |
|
||||
| active device, roster непуст, все own missing/unknown/unavailable | target `on` | unavailable; faded имеет приоритет над working |
|
||||
| active device, roster непуст, все own missing/unknown/unavailable | targets off/unavailable | unavailable + neutral |
|
||||
| virtual controller, roster пуст | target on/off/unavailable | действующий контракт virtual controller без изменений |
|
||||
| ha-disabled/orphaned device, roster пуст | любое | действующий lifecycle-контракт без fallback active-device |
|
||||
|
||||
Живым остаётся состояние, отличное после trim/lowercase от пустой строки,
|
||||
`unknown` и `unavailable`. Числа, `on`, `off`, battery, LQI и update считаются
|
||||
живыми, как в #251.
|
||||
|
||||
Если target aggregate недоступен, controller override меняет availability на
|
||||
`available`, но не создаёт ложный `working`: итог нейтрален. Если цель доступна
|
||||
и включена, target-derived `working` сохраняется.
|
||||
|
||||
### 6.3. Приоритеты
|
||||
|
||||
- critical alarm собственной сущности остаётся выше availability/status;
|
||||
- `live_states: false` и `static_icon` сохраняют нейтральную статичную подачу;
|
||||
- HA-disabled/user-hidden/orphaned lifecycle не оживает от пустого roster;
|
||||
- `controls` определяют status, но не становятся собственными сущностями;
|
||||
- target tombstone сохраняет controller role по #274 и даёт нейтральное лицо;
|
||||
- partial target group и исполнение действия не меняются;
|
||||
- LQI/value badge/температура не создаются из пустого roster.
|
||||
|
||||
### 6.4. A11y, hover и действия
|
||||
|
||||
Доступный marker снова получает обычный hover/focus paint и a11y-state
|
||||
`working` либо neutral вместо `unavailable`. Hit area, click/tap target,
|
||||
confirmation и service calls не меняются. Touch не получает нового жеста;
|
||||
View/kiosk остаются полностью поддержанными.
|
||||
|
||||
## 7. UX
|
||||
|
||||
Новых настроек и элементов нет. Изменение происходит автоматически после
|
||||
обновления frontend: существующий marker перестаёт быть полупрозрачным и
|
||||
использует уже знакомые нейтральную/жёлтую подложки.
|
||||
|
||||
Preview редактора обязан показывать тот же результат для несохранённого draft,
|
||||
построенного из того же active binding и полного sibling roster. Hosted Static
|
||||
показывает ту же live проекцию, оставаясь неинтерактивным согласно своей
|
||||
поверхности. Светлая/тёмная тема меняет только существующие theme tokens.
|
||||
|
||||
## 8. Модель данных, compatibility и migration
|
||||
|
||||
Persisted marker, `controls`, binding, config/model version, backend storage и
|
||||
HA registry не меняются. Миграции нет. Старый frontend продолжит показывать
|
||||
пустой controller roster приглушённым; новый применит правило при следующем
|
||||
render/state tick без записи конфигурации.
|
||||
|
||||
Fallback основан на уже вычисленном `bindingStatus` и не должен добавлять
|
||||
registry fetch, polling либо серверное device-health состояние.
|
||||
|
||||
## 9. i18n и документация
|
||||
|
||||
Новых пользовательских строк нет, поэтому JSON-каталоги en/ru/de не меняются.
|
||||
Обновляются:
|
||||
|
||||
- `docs/DEVICE-PRESENTATION.md`: S05 разделяется на empty-active и
|
||||
non-empty-without-live варианты;
|
||||
- `docs/ARCHITECTURE.md`: уточняется семантика controller availability;
|
||||
- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: отсутствие собственных
|
||||
сущностей у активного устройства не трактуется как offline;
|
||||
- `docs/TESTING.md`: добавляется точная матрица регрессии;
|
||||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: короткий user-visible bullet со
|
||||
ссылкой на #318.
|
||||
|
||||
## 10. Критерии приёмки
|
||||
|
||||
| AC | Требование | Доказательство |
|
||||
|---|---|---|
|
||||
| AC1 | Exact fixture `device:active`, own roster `[]`, `controls=[target]` даёт available+working для target on и available+neutral для off | `test/device-presentation.test.mjs` |
|
||||
| AC2 | Та же fixture при target unavailable/missing и при target-marker tombstone остаётся available+neutral, без ложного working/pulse | unit matrix + production-bundle smoke |
|
||||
| AC3 | Непустой roster, в котором все own states missing/unknown/unavailable, остаётся faded даже при target on; живые battery/LQI/update сохраняют #251 | существующие и новые negative units |
|
||||
| AC4 | `ha_disabled`, `orphaned`, virtual controller, no-controls marker, static icon, live_states off и alarm сохраняют канонические строки таблицы решений | policy/presentation regression suite |
|
||||
| AC5 | Plan и device preview для active empty-roster controller совпадают по `sourceKind`, `visual`, classes и a11y state; target on→off обновляет обе поверхности | расширенный `demo/smoke_wireless_controller_parity.mjs` |
|
||||
| AC6 | Hosted Static использует ту же presentation policy; light/dark и desktop/touch не вводят новый layout/gesture | shared resolver unit + smoke/golden regression |
|
||||
| AC7 | Config round-trip, controls и service-call payload не меняются; backend/model/schema untouched | diff audit + existing config/toggle tests |
|
||||
| AC8 | Мутант, возвращающий empty active roster к `unavailable`, ловится целевым тестом; существующие мутанты #251/#274 продолжают ловиться | `scripts/mutation-gate.mjs` direct run |
|
||||
| AC9 | Fast gates, выбранные targeted smokes, docs check, bundle sync/budget зелёные | точные команды в handoff issue |
|
||||
|
||||
## 11. План реализации и автотестов
|
||||
|
||||
1. В `controllerAvailability()` либо чистом соседнем policy helper различить
|
||||
active physical device с roster `[]` и non-empty roster без live states.
|
||||
2. Не менять `resolvePresentationSources()` и target graph: существующий
|
||||
aggregate уже правильно вычисляет `working/neutral`.
|
||||
3. Добавить unit-матрицу AC1–AC4 в `test/device-presentation.test.mjs` и при
|
||||
необходимости чистый policy test для active-binding gate.
|
||||
4. Расширить `demo/smoke_wireless_controller_parity.mjs` вторым exact fixture
|
||||
без собственных registry rows; проверить plan/preview и on/off/unavailable.
|
||||
5. Добавить узкий mutation guard empty-roster fallback; повторно прогнать
|
||||
`controller-availability-follows-target` и мутанты #274.
|
||||
6. Обновить канонические документы и оба changelog.
|
||||
7. Перед `S7-code-review` выполнить минимум:
|
||||
|
||||
```text
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build
|
||||
npm run bundle:sync
|
||||
npm run bundle:budget
|
||||
node scripts/check-docs.mjs
|
||||
node scripts/smoke-select.mjs --base origin/dev --head HEAD
|
||||
node demo/smoke_wireless_controller_parity.mjs
|
||||
node scripts/mutation-gate.mjs --only <new-empty-roster-mutant>
|
||||
node scripts/mutation-gate.mjs --only controller-availability-follows-target
|
||||
node scripts/mutation-gate.mjs --only wireless-controller-loses-filtered-target-role
|
||||
node scripts/mutation-gate.mjs --only wireless-controller-preview-drops-sibling-markers
|
||||
```
|
||||
|
||||
Если `smoke-select` выберет дополнительные smokes, они также обязательны.
|
||||
Golden baseline не принимается в реализации; полный `golden:verify` остаётся
|
||||
предрелизным gate.
|
||||
|
||||
## 12. Производительность, безопасность и privacy
|
||||
|
||||
Проверка ограничена одним уже построенным marker: `O(1)` для пустого списка и
|
||||
существующий `O(e)` для non-empty roster. Нельзя повторно строить light graph,
|
||||
запрашивать registry/backend или вводить новый cache.
|
||||
|
||||
Действие и service payload не меняются; недоступные цели по-прежнему
|
||||
fail-closed. Новых сетевых запросов, разрешений, логируемых identifiers и
|
||||
персональных данных нет. Решение не должно выводить entity/device IDs в UI.
|
||||
|
||||
## 13. Риски и меры
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| Пустой roster ошибочно оживит disabled/orphaned marker | fallback требует точного `bindingStatus.kind === active`; lifecycle negative tests |
|
||||
| Общий fallback ослабит #251 для event-only/unknown устройств | различать `length === 0` и non-empty; exact negative unit/mutant |
|
||||
| Target unavailable станет ложным working | статус не синтезировать; сохранять target aggregate и проверить AC2 |
|
||||
| Plan и preview снова разойдутся | production-bundle smoke сравнивает полный `ResolvedDevicePresentation` |
|
||||
| Entity marker случайно получит fallback | ограничить active physical `device:` binding и проверить negative fixture |
|
||||
| Изменится Toggle/Glow | не менять соответствующие resolver; existing suites + diff audit |
|
||||
|
||||
## 14. Откат
|
||||
|
||||
Откат — вернуть прежнюю ветку empty roster → `unavailable` и сопровождающие
|
||||
документы/tests. Данных для обратной миграции нет: конфигурация не меняется.
|
||||
Если до беты выяснится, что active device registry недостаточно как fallback,
|
||||
issue возвращается в `S3-spec`; нельзя маскировать риск новым persisted flag без
|
||||
отдельного продуктового решения.
|
||||
|
||||
## 15. Release-артефакты
|
||||
|
||||
- user-visible: **yes**;
|
||||
- один короткий пункт со ссылкой на #318 в RU и EN changelog;
|
||||
- release body упоминает исправление только если оно входит в публикуемую
|
||||
beta/stable range; мелкие внутренние проверки отдельно не перечисляются;
|
||||
- issue закрывает релиз-менеджер после зелёной опубликованной беты;
|
||||
- новые screenshot/golden baseline не требуются, если semantic smoke и
|
||||
существующая visual matrix зелёные без ожидаемой baseline-дельты.
|
||||
|
||||
## 16. Принятые технические предположения — менять свободно
|
||||
|
||||
1. Наиболее узкое место изменения — `controllerAvailability(hass, d)`; перенос
|
||||
предиката в чистую presentation policy допустим, если источник истины один.
|
||||
2. `bindingStatus` — достаточное техническое доказательство active binding; сам
|
||||
HA device registry не объявляется runtime health API.
|
||||
3. Existing `combineVisualSamples()` и controller-face override уже дают
|
||||
правильный status; отдельный новый enum availability не нужен.
|
||||
4. Existing #274 smoke дешевле и надёжнее расширить вторым сценарием, чем
|
||||
создавать ещё один browser process.
|
||||
5. Decision trace может получить отдельный внутренний ID для empty-roster
|
||||
fallback; это developer-facing изменение без persisted schema и i18n.
|
||||
@@ -149,6 +149,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#265](https://github.com/Matysh/houseplan-card/issues/265) Единый контракт ссылочного шва импорта | [265-import-reference-seam.md](265-import-reference-seam.md) |
|
||||
| [#267](https://github.com/Matysh/houseplan-card/issues/267) Таблица решений для «лица» маркера | [267-device-presentation-decision-table.md](267-device-presentation-decision-table.md) |
|
||||
| [#274](https://github.com/Matysh/houseplan-card/issues/274) Беспроводной контроллер одинаково выглядит на плане и в preview | [274-wireless-controller-presentation-parity.md](274-wireless-controller-presentation-parity.md) |
|
||||
| [#318](https://github.com/Matysh/houseplan-card/issues/318) Активный контроллер без собственных сущностей следует `controls` | [318-empty-controller-roster.md](318-empty-controller-roster.md) |
|
||||
| [#294](https://github.com/Matysh/houseplan-card/issues/294) Esc завершает текущую цепочку стен без удаления геометрии | [294-wall-esc-detach.md](294-wall-esc-detach.md) |
|
||||
|
||||
## P3
|
||||
|
||||
Reference in New Issue
Block a user