mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
docs: #44 spec revision 2 — discovery filters UI, preview, excluded-integration reason
User-Visible: no Issue: #44
This commit is contained in:
@@ -1,59 +1,183 @@
|
||||
# ТЗ #44 — Явные фильтры и группировка устройств
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/44
|
||||
- Приоритет: P2
|
||||
- Статус ТЗ: policy proposal ready for owner review
|
||||
- Связано: device inbox #29, field registry #33
|
||||
- Приоритет: P2, tech-debt; полный трек (discovery/UI/settings/i18n/tests —
|
||||
решение аналитики 2026-08-15)
|
||||
- Ревизия: 2 (2026-08-30) — policy v1 принята владельцем (вход в работу без
|
||||
возражений к записанной политике); конкретизация под инбокс v1.69
|
||||
- Связано: device inbox #29, field registry/манифест #33 (паспорта
|
||||
`allow-extra` обоих ключей уже выданы), персист-паттерн #377
|
||||
|
||||
## Цель
|
||||
## Сценарий
|
||||
|
||||
Убрать скрытые настройки, влияющие на discovery. Каждый ключ либо получает
|
||||
поддерживаемый advanced UI и documented default, либо мигрирует в фиксированное
|
||||
product rule.
|
||||
Хозяин плана открывает «Устройства» и на вкладке «Доступные» видит новый
|
||||
раскрываемый блок «Фильтры обнаружения». Там два честных элемента: тумблер
|
||||
«Объединять светильники комнаты в группу» (включён по умолчанию — как продукт
|
||||
и вёл себя всегда) и список «Исключённые интеграции» с рекомендованным
|
||||
набором, поиском по реально присутствующим в HA интеграциям и кнопкой
|
||||
«Вернуть рекомендуемые». Изменение показывает счётчики «появится / скроется /
|
||||
сгруппируется» до сохранения; Сохранить пишет настройки один раз.
|
||||
|
||||
## Решение v1
|
||||
## Что человек увидит до и после
|
||||
|
||||
Оба текущих ключа остаются поддерживаемыми и становятся видимыми в Advanced
|
||||
разделе inbox:
|
||||
**До**: группировка света и исключение интеграций происходят молча; понять
|
||||
«почему этого устройства нет в списке» нельзя. **После**: обе настройки видны
|
||||
и объяснимы; у кандидата, скрытого фильтром, в инбоксе появляется причина
|
||||
«Исключена интеграция X»; ничего не меняется само — только по Сохранить.
|
||||
|
||||
### `group_lights`
|
||||
## Проблема
|
||||
|
||||
- default `true`;
|
||||
- label: «Объединять несколько светильников комнаты»;
|
||||
- preview показывает, какие bindings будут группой/отдельными marker;
|
||||
- переключение не применяется до Save и не удаляет explicit markers;
|
||||
- existing light-group marker сохраняет lifecycle; конфликт bindings preview.
|
||||
Оба ключа — «третий вариант», запрещённый issue: влияют на результат, не
|
||||
видны, без migration policy. Факты кода: `group_lights` читается как
|
||||
`!== false` (default TRUE, `devices.ts:1033/1098`); `exclude_integrations`
|
||||
при наличии ЦЕЛИКОМ подменяет продуктовый `EXCLUDED_DOMAINS` (13 доменов,
|
||||
`rules.ts:12`; потребители `houseplan-card.ts:3924`, `space-render.ts:245`,
|
||||
discovery `devices.ts:1491`).
|
||||
|
||||
### `exclude_integrations`
|
||||
## Решение (policy v1, принята)
|
||||
|
||||
- default — текущий `EXCLUDED_DOMAINS`/product list;
|
||||
- UI — searchable multi-select интеграций, реально присутствующих в registry,
|
||||
плюс reset to recommended defaults;
|
||||
- изменение влияет только на automatic candidates/seed, не скрывает explicit
|
||||
live marker и не удаляет tombstone;
|
||||
- reason в inbox — «Исключена интеграция X».
|
||||
Оба ключа — поддерживаемые advanced-настройки. Storage-имена и семантика НЕ
|
||||
меняются (обратная совместимость): `group_lights` отсутствие = true;
|
||||
`exclude_integrations` отсутствие = продуктовый список, наличие = полная
|
||||
замена. UI прячет эту механику за честными представлениями.
|
||||
|
||||
Название storage key `exclude_integrations` сохраняется для compatibility;
|
||||
семантика и default фиксируются в registry #33.
|
||||
## Скоуп
|
||||
|
||||
## Исследование перед включением Save
|
||||
### 1. Блок «Фильтры обнаружения» в инбоксе
|
||||
|
||||
Локальный config-audit считает значения/отклонения от defaults без вывода ids.
|
||||
Synthetic fixtures моделируют реальные классы: group lights, parent device,
|
||||
service/bridge, explicit override, tombstone. Если audit показывает, что ключ
|
||||
невозможно объяснить без вредного поведения, owner может отдельным решением
|
||||
перевести его в fixed rule до реализации UI.
|
||||
- Раскрываемая секция внизу вкладки «Доступные» диалога «Устройства»
|
||||
(editor-runtime; холодный View не задет — весь UI в runtime, класс #357).
|
||||
- **Тумблер группировки**: отражает `settings.group_lights` (unset→вкл).
|
||||
Выключение НЕ удаляет существующие маркеры (в т.ч. group-маркеры) — меняет
|
||||
только будущих кандидатов discovery.
|
||||
- **Исключённые интеграции**: чипы текущего действующего набора (ключ или
|
||||
продуктовый список); поиск-добавление по интеграциям, реально
|
||||
присутствующим в registry HA (platform-ы entity registry); удаление чипа;
|
||||
кнопка «Вернуть рекомендуемые» (= удалить ключ из settings). Если
|
||||
действующий набор равен продуктовому — ключ в settings отсутствует
|
||||
(паттерн «дефолт = отсутствие ключа», #377).
|
||||
- **Превью до сохранения**: счётчики «появится N / скроется M /
|
||||
группировка изменит K» — пересчёт кандидатов discovery на черновых
|
||||
значениях (чистая функция buildDevices уже принимает settings/excluded
|
||||
через ctx — используется она же, без дублирования логики). Список
|
||||
затронутых биндингов раскрывается (имена, без лишних id).
|
||||
- **Транзакция**: Изменения живут в черновике диалога; Сохранить пишет
|
||||
settings ОДИН раз штатным сериализованным путём (expected_rev #340);
|
||||
Отмена — no-op. Undo — штатный именованный снапшот конфига (паттерн
|
||||
существующих правок настроек).
|
||||
|
||||
## UX/transaction
|
||||
### 2. Причина «Исключена интеграция X» в инбоксе
|
||||
|
||||
Preview diff: новые/скрытые/grouped candidates counts и конкретный список в
|
||||
локальной session. Apply пишет settings один раз, rebuild devices и создаёт
|
||||
named undo/config snapshot. Cancel no-op. Два клиента используют config rev.
|
||||
Кандидат, отфильтрованный исключением, при показе на вкладке «Скрытые»
|
||||
несёт `DeviceInboxReason` нового значения `excluded_integration` с текстом
|
||||
«Исключена интеграция {integration}». (Сегодня такие устройства не попадают
|
||||
в инбокс вовсе — они появляются на «Скрытых» именно с этой причиной;
|
||||
объём выдачи ограничен существующей механикой вкладки.)
|
||||
|
||||
## Приёмка
|
||||
### 3. Registry/доки
|
||||
|
||||
- ни один runtime discovery key не скрыт от пользователя/registry;
|
||||
- defaults совпадают frontend, backend, docs и fixtures;
|
||||
- explicit marker никогда не исчезает из-за filter change;
|
||||
- preview объясняет каждую изменившуюся binding;
|
||||
- old config даёт прежний result до user action.
|
||||
- `config-field-registry.mjs`: статусы обоих ключей `decision-required` →
|
||||
`current`, `ui: 'device inbox → Discovery filters'`, `enforcedBy` — новый
|
||||
смок; migration-строки — «none, supported setting».
|
||||
- USER-GUIDE(.ru): подраздел «Фильтры обнаружения» в теме инбокса.
|
||||
|
||||
## Не-скоуп
|
||||
|
||||
- Изменение семантики replacement→additive у `exclude_integrations`
|
||||
(совместимость; additive-режим — отдельное решение, если понадобится).
|
||||
- Пер-пространственные фильтры; фильтры по доменам сущностей.
|
||||
- Изменение самого продуктового списка `EXCLUDED_DOMAINS`.
|
||||
- Материализация/`show_all` (закрыто ранее, #33 registry).
|
||||
|
||||
## Контракт поведения
|
||||
|
||||
1. Конфиг без ключей → поведение байт-в-байт сегодняшнее (default-путь не
|
||||
меняется ни в одном потребителе).
|
||||
2. Explicit-маркер (живой или tombstone) НИКОГДА не исчезает из-за смены
|
||||
фильтров — фильтры влияют только на автоматических кандидатов и seed.
|
||||
3. Оба значения читаются из одного источника (`_settings`) всеми
|
||||
потребителями — новых копий состояния нет.
|
||||
4. `Вернуть рекомендуемые` удаляет ключ (не пишет копию списка).
|
||||
5. Никакой записи до явного Сохранить; двойная вкладка — штатный conflict.
|
||||
|
||||
## UX / i18n
|
||||
|
||||
Новые ключи (en/ru/de/fr, все четыре — паритет-гейт): заголовок секции,
|
||||
label+hint тумблера, label списка, placeholder поиска, кнопка reset,
|
||||
счётчики превью (3), причина `device_inbox.reason_excluded_integration`
|
||||
(с плейсхолдером {integration}). Порядка 9 ключей.
|
||||
|
||||
## Модель данных и миграция
|
||||
|
||||
Нет изменений хранения: ключи те же, семантика та же. Бэкенд-схему не
|
||||
трогаем: ключи остаются `allow-extra` (паспорта #33). Причина: перенос в
|
||||
строгую схему — отдельное решение с рисками отклонения старых конфигов;
|
||||
issue требует видимости и policy, не пере-схемизации.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- **AC1** (смок): тумблер группировки выключить+Сохранить → в конфиге
|
||||
`group_lights: false`, повторное открытие показывает выкл, discovery-
|
||||
кандидаты содержат отдельные лампы вместо группы; обратное включение →
|
||||
ключ УДАЛЯЕТСЯ из settings (default = отсутствие).
|
||||
- **AC2** (смок): добавление интеграции в исключения → превью-счётчик
|
||||
«скроется» ненулевой; Сохранить → ключ в settings = полный действующий
|
||||
список; «Вернуть рекомендуемые» + Сохранить → ключа нет.
|
||||
- **AC3** (смок): у явного маркера с binding исключённой интеграции ничего
|
||||
не меняется (живой остаётся на плане, tombstone не трогается).
|
||||
- **AC4** (смок): скрытый фильтром кандидат виден на «Скрытых» с причиной
|
||||
«Исключена интеграция X».
|
||||
- **AC5** (юнит): резолвер действующего набора: unset→продуктовый список,
|
||||
[]→пусто (валидная «ничего не исключать»), список→список; тумблер:
|
||||
unset→true.
|
||||
- **AC6** (юнит): превью-диффер чист: те же ctx-входы, что у boевого
|
||||
buildDevices (контракт-проверка, что превью не дублирует логику фильтра).
|
||||
- **AC7**: отсутствие ключей → discovery-выдача байт-в-байт с сегодняшней
|
||||
(регресс-юнит на фикстуре).
|
||||
- **AC8**: полный гейт; i18n 4/4; бюджет: UI в editor-runtime (lazy),
|
||||
initial ≈ без изменений.
|
||||
|
||||
## План автотестов
|
||||
|
||||
- Юниты: резолверы (AC5), превью-контракт (AC6), регресс дефолта (AC7) —
|
||||
test/devices.test.mjs + test/device-inbox.test.mjs.
|
||||
- Смок `demo/smoke_discovery_filters.mjs` (новый): AC1–AC4 на демо-стенде
|
||||
с моковым config/set (паттерн #377-смока).
|
||||
- Мутанты: м1 — превью-счётчики от копии логики (подмена вызова общего
|
||||
билдера константой) → красный AC6/смок; м2 — «Вернуть рекомендуемые»
|
||||
пишет копию списка вместо удаления ключа → красный AC2.
|
||||
|
||||
## Риски
|
||||
|
||||
- Пересчёт превью на большом registry — переиспользуется кэшируемый
|
||||
buildDevices; расчёт по явному действию (открытие секции/изменение), не
|
||||
на каждый тик.
|
||||
- Реason-вкладка «Скрытые» получает новый класс записей — объём ограничен
|
||||
существующим механизмом вкладки; смок проверяет отсутствие дублей.
|
||||
- Гонка вкладок — штатный conflict #340.
|
||||
|
||||
## Откат
|
||||
|
||||
`git revert`: UI и причина исчезают, ключи продолжают работать как до
|
||||
задачи (скрыто); записанные пользователями значения остаются валидными —
|
||||
семантика не менялась. Потери данных нет.
|
||||
|
||||
**DoR-примечания:** миграция/compatibility — нет (семантика и имена ключей
|
||||
неизменны); touch — стандартные элементы диалога (чипы/тумблер), новых
|
||||
жестов нет; производительность — превью по явному действию.
|
||||
|
||||
## Release-артефакты
|
||||
|
||||
- CHANGELOG + CHANGELOG.ru: user-visible запись (настройки фильтров стали
|
||||
видимыми и объяснимыми), ссылка #44.
|
||||
- USER-GUIDE(.ru): подраздел; ARCHITECTURE.md: абзац про policy «нет скрытых
|
||||
discovery-ключей» со ссылкой на registry.
|
||||
|
||||
## Принятые предположения
|
||||
|
||||
- Размещение — вкладка «Доступные» (фильтры определяют её содержимое);
|
||||
если ревью ТЗ предпочтёт отдельную вкладку/шестерёнку — правится без
|
||||
смены механики.
|
||||
- `[]` (пустой список) — валидное значение «ничего не исключать», отличное
|
||||
от отсутствия ключа; так уже работает код (`list ? new Set(list) : EXCLUDED_DOMAINS`).
|
||||
- Причина показывается на «Скрытых», НЕ в отдельной новой вкладке.
|
||||
|
||||
Reference in New Issue
Block a user