Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в `legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код, тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ — на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`), DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди перенесённых нет. Относительные ссылки перенесённых файлов переписаны (`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) — все 26 резолвятся. Попутно: битая ссылка в `089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` — теперь команда `git show` по истории. Строка в `legacy/README.md`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
27 KiB
Issue #251 — доступность контроллера не наследуется от управляемой цели
- Дата: 2026-08-23
- Тип: bug + polish · приоритет P2
- Оценка: пользовательская ценность 7/10 · сложность 4/10 · риск 4/10
- Issue: #251
- Связанные контракты: #107, #174, #178
- Ветка:
issue/251-controller-target-availability - Статус ТЗ: на ревью
Канонические документы: docs/SCOPE.md, docs/ARCHITECTURE.md,
docs/CONFIG-COMPATIBILITY.md, docs/TOUCH-SUPPORT.md,
docs/USER-GUIDE.md, docs/USER-GUIDE.ru.md, docs/TESTING.md.
1. Сценарий и персона
Житель дома смотрит на план с батарейным беспроводным выключателем, который управляет лампой или световой группой. Сам выключатель остаётся в Zigbee-сети: его батарея и LQI обновляются, но управляемая лампа временно недоступна.
Человек ожидает, что внешний вид маркера отвечает на вопрос «доступен ли сам выключатель», а жёлтая подложка — «работает ли управляемый свет». При нажатии на маркер недоступной цели он должен получить понятное объяснение безопасного no-op, а не молчание.
Поверхности: интерактивные View и kiosk; общий presentation contract также обязан совпасть в hosted Static и preview редактора устройств там, где включены live states.
2. Что человек увидит до и после
До: живая батарейная кнопка становится полупрозрачной, когда недоступна её лампа, и нажатие ничего не делает и ничего не объясняет.
После: живая кнопка остаётся обычной нейтральной, а не жёлтой; нажатие показывает «Цель „…“ недоступна — действие не выполнено». Полупрозрачность появляется только когда нет живого состояния у самого контроллера.
3. Проблема и подтверждённый диагноз
resolvePresentationSources() для маркера с внешними controls строит
sourceKind: controls и передаёт в combineVisualSamples() состояния целей.
Если все цели имеют unknown / unavailable либо отсутствуют в hass.states,
aggregate получает availability: unavailable, после чего общий presentation
добавляет класс unavail. Собственные DevItem.entities контроллера в этой
ветке не участвуют, поэтому живые battery и linkquality ничего не меняют.
Действие уже fail-closed: resolveToggleIntent() исключает недоступные цели,
хранит их имена и причины, а toggleOperation() не создаёт service call, когда
доступных целей нет. _clickDevice() завершает такую ветку ранним return,
поэтому пользователь не получает объяснения.
HA не имеет единого device state. Решением владельца доступность контроллера выводится отдельно из его собственных активных entity states.
4. Зафиксированное решение владельца
- Внешний вид маркера не наследует доступность управляемой цели.
working/onпо-прежнему наследуется от доступных управляемых целей: хотя бы одна включённая цель делает контроллер жёлтым.- Если ни одна цель не доступна, target-derived состояние нейтрально и не может сделать контроллер жёлтым.
- Полупрозрачность означает недоступность самого привязанного контроллера.
- Контроллер доступен, если хотя бы одна его собственная активная сущность
имеет непустое состояние, отличное от
unknownиunavailable. Диагностическиеbattery/linkqualityиupdateучаствуют;controls— нет. - Если доступных целей для toggle нет из-за их недоступности, нажатие не отправляет команду и показывает локализованное уведомление с именем цели и фактом, что действие не выполнено.
- Новый бейдж или отдельный постоянный индикатор недоступной цели не вводится.
Продуктовых вопросов не осталось.
5. Скоуп
Входит
- отдельное вычисление availability собственного HA-контроллера;
- target-derived
working/onбез target-derivedunavailable; - battery/LQI/update как допустимые доказательства доступности контроллера;
- нейтральный вид при живом контроллере и полностью недоступной группе;
- полупрозрачный вид при полностью недоступных собственных сущностях даже при доступной или включённой цели;
- виртуальные контроллеры с
controlsкак всегда доступные, поскольку у них нет HA-устройства, которое можно объявить offline; - уведомление для all-unavailable/missing/HA-disabled configured targets;
- повторная проверка доступности при подтверждении toggle;
- View/kiosk, shared presentation surfaces, i18n, accessibility, unit, smoke и semantic golden coverage;
- документация и changelog RU/EN.
Не входит
- новый бейдж, warning glyph, цвет или pulse для недоступной цели;
- изменение Glow, Light fill, комнатной статистики или spatial ownership;
- изменение групповой семантики «any on → all off, all off → all on»;
- service call в недоступную, отсутствующую или HA-disabled цель;
- уведомление о каждом пропуске, если доступная часть группы была выполнена;
- изменение quiet no-op для secure, unsupported или вообще не настроенной цели;
- новый HA device-health API, ping или network probe;
- изменение long-press, right-click, info card либо редактора controls;
- persisted schema, migration, backend или публикация hidden Iso.
6. Контракт presentation
6.1. Два независимых факта
Для sourceKind: controls вычисляются отдельно:
controllerAvailability = availability(own active DevItem.entities)
targetStatus = status(available resolved controls)
Итоговое лицо использует availability первого и working / neutral второго.
Цели не входят в own entity set даже если находятся в том же HA device.
Собственная сущность считается живой, если запись состояния существует и
String(state).trim().toLowerCase() не равна пустой строке, unknown или
unavailable. Значения on, off, idle, числовые строки, battery, LQI и
состояние update являются живыми. Registry row без live state недостаточна.
Матрица обязательного результата при включённых live states:
| Собственный контроллер | Управляемые цели | Итог |
|---|---|---|
| battery=100, LQI=164 | все unavailable |
available + neutral |
| хотя бы одна own entity живая | хотя бы одна target on |
available + working |
| хотя бы одна own entity живая | все target off |
available + neutral |
| все own missing/unknown/unavailable | target on |
unavailable; класс unavail имеет приоритет над working |
| все own missing/unknown/unavailable | все targets unavailable | unavailable + neutral |
| virtual controller без own entities | все targets unavailable | available + neutral |
event.* = unknown само по себе не доказывает online. Если других живых
собственных сущностей нет, HA-контроллер недоступен. Для exact entity: binding
own set содержит только сущности этой привязки; для device: — его активный
DevItem.entities после действующих registry/ownership filters.
6.2. Приоритеты и существующие исключения
- критический alarm собственной сущности сохраняет высший приоритет;
- при
live_states: falseостаётся существующее статичное нейтральное лицо; - display
static_iconпо-прежнему намеренно игнорирует все live states; - HA-disabled/user-hidden markers сохраняют текущую видимость и editor-preview семантику;
- media-player
offи composite Power contracts вне controls-ветки не меняются; - partial target availability: доступная часть определяет working и действие, недоступная часть игнорируется так же, как до задачи.
Glow, room fill и statistics продолжают читать resolved light graph целиком. Разделяется только presentation availability контроллера.
7. Контракт действия и уведомления
7.1. Полностью недоступная configured group
Если explicit Toggle имеет сохранённые цели, но toggleOperation(intent) не
существует и все пропуски объясняются unavailable, missing или
ha-disabled, клик/тап:
- не вызывает
callService/callWS; - не запускает press feedback;
- не открывает info card или confirmation dialog;
- показывает существующий локальный House Plan toast с
role="alert"иaria-live="assertive".
Фразу владельца «стандартными средствами HA» здесь намеренно реализует
существующий локальный toast карточки по образцу toast.error,
toast.tap_target_changed и других системных объяснений House Plan. Новый
нативный глобальный hass-notification не вводится.
Точный текст:
| Случай | RU | EN |
|---|---|---|
| одна цель | Цель «{name}» недоступна — действие не выполнено |
Target “{name}” is unavailable — no action was performed |
| несколько | Цели недоступны: {names}. Действие не выполнено |
Targets are unavailable: {names}. No action was performed |
name берётся из HA/marker friendly name, с fallback на entity/ref. names
перечисляет уникальные цели в deterministic intent order через запятую. Строка
рендерится как текст, не HTML.
unknown цели относится к unavailable. missing и ha-disabled не получают
ложно более точной причины: для человека цель недоступна. Если no-op вызван
secure, unsupported, отсутствием configured target или отсутствующим
service capability, существующее поведение сохраняется.
Для смешанного нулевого набора secure + unavailable/missing/HA-disabled toast
показывается по именам только недоступных целей; secure-цели не называются и не
становятся исполняемыми. Наличие unsupported среди пропусков сохраняет
существующий no-op без этого toast.
7.2. Partial group и гонки
Если доступна хотя бы одна цель, выполняется ровно существующее доступное подмножество. Toast «ничего не произошло» не показывается, потому что действие произошло; skipped targets остаются в текущей подсказке/confirmation.
При tap_confirm: true:
- полностью недоступная группа показывает toast сразу и не открывает диалог;
- если цель стала недоступна между открытием и подтверждением, повторный resolve показывает тот же unavailable toast и не вызывает сервис;
- если executable target set изменился иначе, сохраняется текущий
toast.tap_target_changed; - ошибка HA после уже начатого service call остаётся общим
toast.error.
Доступность самого контроллера не блокирует dashboard action: если цель доступна, явный tap по его маркеру продолжает управлять целью. Полупрозрачность описывает физический контроллер, а tap выполняется интерфейсом House Plan.
8. UX, accessibility и touch
Нового компонента нет. Используется существующий toast, поэтому focus не перемещается, модальный слой не создаётся, а сообщение объявляется screen reader как assertive alert.
Контракт одинаков для mouse click и короткого touch tap. Long-press, pan, pinch, drag, pointercancel и kiosk swipe не могут показать toast или отправить команду. На unavailable/no-op нет 5% press feedback, потому что команда не отправлена.
Hosted Static получает правильную нейтральную/полупрозрачную проекцию, но не новое действие, если поверхность не интерактивна. Preview редактора показывает тот же live face без toast.
9. Модель данных, compatibility и migration
Persisted config, marker.controls, bindings, registry projection, store/model
version и backend schema не меняются. Миграции нет: существующие маркеры
получают новое поведение при следующем render/state tick.
Старый frontend продолжит наследовать target availability, новый разделит её; конфиг остаётся двусторонне читаемым. Controls не переписываются и не нормализуются этой задачей.
10. Производительность и безопасность
Own availability вычисляется за O(e) по уже построенному d.entities одного
маркера и O(1) дополнительной памяти. Нельзя повторно строить plan-wide light
graph, обращаться к registry/backend или вводить новый render cache. Оценка
должна использовать тот же HA snapshot, что и presentation.
Действие остаётся fail-closed: недоступные цели не попадают в service payload.
Friendly names выводятся через Lit text binding; innerHTML и доверенный HTML
не используются. Новых сетевых запросов, разрешений или чувствительных данных
нет.
11. Критерии приёмки
| AC | Требование | Доказательство |
|---|---|---|
| AC1 | Матрица §6.1 даёт точные availability/status/classes для battery/LQI, event-only, own missing, target on/off/unavailable и virtual controller | test/device-presentation.test.mjs, основной mutation guard |
| AC2 | Target availability больше не участвует в controller availability, но target on по-прежнему даёт working, partial group сохраняет доступную семантику, alarm/live_states/static_icon приоритеты не меняются |
presentation unit matrix + existing #107/#174 tests |
| AC3 | Клик по одной полностью недоступной configured target показывает точный локализованный toast с friendly name, без service/WS, confirm, info card и press feedback | pure action unit + production-bundle smoke_controls mouse/touch |
| AC4 | Несколько недоступных целей дают plural toast в deterministic order; mixed group вызывает сервис только для доступного подмножества и не показывает no-op toast | toggle unit + browser smoke |
| AC5 | Цель, ставшая unavailable до подтверждения, даёт unavailable toast; иной executable target-set change сохраняет toast.tap_target_changed; service rejection сохраняет toast.error |
confirmation unit/smoke с live state mutation |
| AC6 | View, kiosk, hosted Static и editor preview показывают одинаковое лицо; light/dark и desktop/touch не вводят новый badge, Glow или layout shift | targeted smoke + semantic golden light/dark |
| AC7 | Config round-trip, controls order/content, backend/schema/model versions и virtual-light state не меняются | existing config/device tests + diff review |
| AC8 | Руководства объясняют независимость availability и working, оба changelog ссылаются на #251, i18n RU/EN синхронен | check-docs, i18n tests, code review |
| AC9 | Implementation loop зелёный, три поставляемые bundle-копии побайтово одинаковы | typecheck, unit, build, bundle parity |
12. План реализации и автотестов
- В
src/device-presentation.tsдобавить pure own-availability projection из активныхDevItem.entitiesи применять её только к controls-derived face, не меняя resolved light graph и critical sources. - Сохранить target aggregate для
working/neutral/activity, но исключить из итоговой availability. Явно покрыть virtual, exact entity, device binding, partial group, live_states и static_icon. - В
src/device-toggle.tsвыделить pure классификацию no-op, которому положен unavailable toast, и deterministic список display names. Не связывать UI с сырыми внутренними причинами напрямую. - В
_clickDevice()использовать одну ветку уведомления при первичном tap и при confirm re-resolution. Не запускать feedback до существования operation. - Добавить две пары i18n-ключей RU/EN для singular/plural текста и тест parity.
- Расширить
test/device-presentation.test.mjs,test/device-toggle.test.mjsи production-bundle smoke controls матрицей AC. - Добавить/уточнить semantic golden с живой battery-кнопкой и unavailable target в light/dark; baseline принимается только в предбетовый цикл.
- Обновить архитектуру, руководства, testing matrix и changelog RU/EN.
В implementation loop запускаются только typecheck, unit, build.
Targeted smoke, mutation и docs/process gates выполняются перед code review;
полные golden/smoke/performance — перед бетой по общему процессу.
13. Mutation guards
| id | Что ломает | Что обязано покраснеть |
|---|---|---|
controller-availability-follows-target |
возвращает availability target aggregate вместо own entities | AC1 presentation unit и AC6 semantic golden |
controller-diagnostics-do-not-prove-online |
исключает battery/LQI/update из own availability | исходный field fixture AC1 |
unavailable-toggle-stays-silent |
восстанавливает ранний quiet return | AC3 action unit и smoke |
partial-group-shows-noop-toast |
показывает «ничего не произошло» после частичного service call | AC4 unit/smoke |
Мутанты не должны подменять production conditions тестовыми issue-флагами.
14. Release-артефакты
Implementation commit пользовательски видим и получает User-Visible: yes.
В том же коммите обязательны:
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdсо ссылкой на #251;docs/ARCHITECTURE.md— граница controller availability / target status;docs/USER-GUIDE.mdиdocs/USER-GUIDE.ru.md— поведение controls, полупрозрачности и no-op toast;docs/TESTING.md— presentation/action/smoke/golden matrix;docs/STATUS.mdтолько если unreleased-сводка перечисляет исправления этого уровня;- RU/EN i18n, unit/smoke fixtures, mutation manifest и semantic golden guard;
- синхронные production bundles.
Изменение src/** требует актуального screenshot provenance по workflow
Docs screenshots. Если существующие снимки не содержат этот state, пиксели
документации не меняются, но проверяемый provenance остаётся обязательным.
Golden baseline принимается только перед бетой из reviewed Linux artifact.
Новых schema/model migration, backend, security или отдельного performance артефакта нет.
15. Откат
Одна code revision возвращает прежнее target-derived availability и тихий no-op. Данные не меняются, поэтому обратная миграция не нужна. Откат возвращает ложную полупрозрачность и отсутствие объяснения, но не повреждает конфиг.
16. Риски
- Рабочая цель перестаёт делать контроллер жёлтым. Митигация: AC1/AC2 независимо проверяют status и availability.
- Event-only контроллер ложно считается online. Митигация:
unknownне является живым состоянием; отдельная fixture. - Диагностика исключена как не-functional entity. Митигация: own availability намеренно читает активные entities, а не functional role.
- Partial group получает ложное «ничего не произошло». Митигация: toast разрешён только при отсутствии operation; AC4 и mutant.
- Confirm race остаётся с общим сообщением. Митигация: повторный intent классифицируется до generic target-changed branch.
- Исправлен View, но Static/preview расходятся. Митигация: один shared presentation resolver, AC6.
- Toast раскрывает сырую/опасную строку. Митигация: friendly-name fallback и Lit text binding без HTML.
17. Принятые предположения — технические, менять свободно
- Own availability удобнее представить отдельным pure helper/result, а не
добавлять собственные entities в
visualSources: последнее смешало бы ихworkingс состоянием цели. - Набор
d.entitiesуже является активной registry/ownership projection и не требует повторной registry-фильтрации. - Singular/plural выбирается по числу уникальных skipped targets после resolver de-duplication; порядок берётся из intent.
- Для UI и тестов предпочтителен общий pure helper no-op classification, чтобы первичный tap и confirm race не разошлись.
- Новый semantic golden можно добавить в существующую device-state matrix,
если он явно различает прежний
unavailи новый neutral state в обеих темах.
Не являются предположениями: собственная доступность по любому живому own entity state, участие battery/LQI/update, исключение controls из availability, сохранение target-derived working, отсутствие нового badge и уведомление при полностью невыполнимом unavailable toggle — решения владельца.