Files
houseplan-card/docs/specs/044-filter-grouping-policy.md
T

14 KiB
Raw Blame History

ТЗ #44 — Явные фильтры и группировка устройств

  • Issue: https://github.com/Matysh/houseplan-card/issues/44
  • Приоритет: 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

Сценарий

Хозяин плана открывает «Устройства» и на вкладке «Доступные» видит новый раскрываемый блок «Фильтры обнаружения». Там два честных элемента: тумблер «Объединять светильники комнаты в группу» (включён по умолчанию — как продукт и вёл себя всегда) и список «Исключённые интеграции» с рекомендованным набором, поиском по реально присутствующим в HA интеграциям и кнопкой «Вернуть рекомендуемые». Изменение показывает счётчики «появится / скроется / сгруппируется» до сохранения; Сохранить пишет настройки один раз.

Что человек увидит до и после

До: группировка света и исключение интеграций происходят молча; понять «почему этого устройства нет в списке» нельзя. После: обе настройки видны и объяснимы; у кандидата, скрытого фильтром, в инбоксе появляется причина «Исключена интеграция X»; ничего не меняется само — только по Сохранить.

Проблема

Оба ключа — «третий вариант», запрещённый 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).

Решение (policy v1, принята)

Оба ключа — поддерживаемые advanced-настройки. Storage-имена и семантика НЕ меняются (обратная совместимость): group_lights отсутствие = true; exclude_integrations отсутствие = продуктовый список, наличие = полная замена. UI прячет эту механику за честными представлениями.

Скоуп

1. Блок «Фильтры обнаружения» в инбоксе

  • Раскрываемая секция внизу вкладки «Доступные» диалога «Устройства» (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 — штатный именованный снапшот конфига (паттерн существующих правок настроек).

2. Причина «Исключена интеграция X» в инбоксе

Кандидат, отфильтрованный исключением, при показе на вкладке «Скрытые» несёт DeviceInboxReason нового значения excluded_integration с текстом «Исключена интеграция {integration}». (Сегодня такие устройства не попадают в инбокс вовсе — они появляются на «Скрытых» именно с этой причиной; объём выдачи ограничен существующей механикой вкладки.)

3. Registry/доки

  • 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).
  • Причина показывается на «Скрытых», НЕ в отдельной новой вкладке.