Files
houseplan-card/docs/specs/318-empty-controller-roster.md
T
2026-08-28 13:07:58 +00:00

21 KiB
Raw Blame History

Issue #318 — активный контроллер без собственных сущностей следует controls

  • Дата: 2026-08-28
  • Тип: bug · приоритет P2
  • Оценка: пользовательская ценность 6/10 · сложность 3/10 · риск 4/10
  • Issue: #318
  • Связанные контракты: #251, #267, #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 выполнить минимум:
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 --id=<new-empty-roster-mutant>
node scripts/mutation-gate.mjs --id=controller-availability-follows-target
node scripts/mutation-gate.mjs --id=wireless-controller-loses-filtered-target-role
node scripts/mutation-gate.mjs --id=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.