mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
560 lines
37 KiB
Markdown
560 lines
37 KiB
Markdown
# ТЗ #29 — единый каталог и жизненный цикл устройств
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/29
|
||
- Приоритет: P1
|
||
- Тип: feature
|
||
- Трек: обычный
|
||
- Связано: #44 (настройки discovery), #109 (поиск каналов устройства),
|
||
#126 (смена HA area), #262 (возврат exact entity после удаления device)
|
||
|
||
## 1. Сценарий
|
||
|
||
**Персона:** администратор дома из `docs/SCOPE.md`.
|
||
|
||
**Поверхность:** полный `houseplan-card`, desktop-first редактор устройств.
|
||
|
||
**Момент:** пользователь добавил, скрыл или удалил устройства, Home Assistant
|
||
обнаружил новые или деактивировал существующие, и теперь нужно понять, какие
|
||
объекты представлены на плане и что с каждым из них можно сделать.
|
||
|
||
## 2. Что человек увидит до и после
|
||
|
||
**До:** в панели отдельно находятся «Добавить» и «Скрытые и
|
||
деактивированные»; они позволяют выполнить операцию, но не дают единой картины
|
||
и не объясняют, почему конкретного устройства нет на плане.
|
||
|
||
**После:** одна кнопка «Устройства» открывает каталог всех относящихся к плану
|
||
HA-привязок с поиском, понятной категорией, причиной текущего состояния и
|
||
доступным следующим действием; автоматически появившееся устройство уже
|
||
считается находящимся на плане и отдельно помечается как новое.
|
||
|
||
## 3. Проблема
|
||
|
||
Текущая реализация уже поддерживает несколько устойчивых состояний:
|
||
|
||
- автоматически отображаемое устройство без сохранённого `marker`;
|
||
- явный видимый marker;
|
||
- пользовательски или автоматически скрытый `hidden:true` marker;
|
||
- временно неактивную HA-привязку (`ha_disabled`, `orphaned`, `unverified`);
|
||
- удалённую exact binding с `removed:true`, которую можно добавить заново;
|
||
- отдельную `entity:X`, возвращённую из-под tombstone родителя `device:D`.
|
||
|
||
Эти состояния обслуживаются разными UI-путями и не образуют объяснимого
|
||
каталога. Пользователь вынужден помнить разницу между скрытием, удалением,
|
||
деактивацией в HA, фильтрацией и автоматическим появлением. В результате
|
||
одинаковое отсутствие значка визуально выглядит как несколько разных причин,
|
||
а поиск уже размещённого устройства возможен только глазами на плане.
|
||
|
||
Старое ТЗ от 9 августа дополнительно считало любую binding без marker «новой».
|
||
Это расходится с текущим zero-config поведением: автообнаруженное устройство в
|
||
привязанной HA area уже рисуется без marker.
|
||
|
||
## 4. Цель и метрики результата
|
||
|
||
Создать одну read-only при открытии проекцию lifecycle, которая использует
|
||
существующие данные и действия, а не вводит второй источник истины.
|
||
|
||
Результат считается достигнутым, если:
|
||
|
||
1. для каждой включённой в каталог exact binding существует ровно одна базовая
|
||
категория;
|
||
2. каталог отличает пользовательское намерение от временного статуса HA;
|
||
3. открытие, поиск, смена вкладки и просмотр причины не меняют config, layout и
|
||
их revisions;
|
||
4. все существующие операции — найти, настроить, добавить, показать и добавить
|
||
заново — доступны из одной поверхности;
|
||
5. текущая автоматическая раскладка, tombstones, exact entity ownership и
|
||
фильтрация не меняют семантику.
|
||
|
||
## 5. Скоуп
|
||
|
||
Входит:
|
||
|
||
- единый каталог в редакторе устройств;
|
||
- pure resolver строк каталога;
|
||
- категории, статусы, причины, счётчики и поиск;
|
||
- действия над одной строкой;
|
||
- возврат из существующего диалога устройства обратно в каталог;
|
||
- локальное выделение найденного marker на плане;
|
||
- desktop, узкая responsive-компоновка без горизонтального скролла, keyboard
|
||
navigation;
|
||
- RU/EN i18n, unit/smoke/golden, пользовательская и каноническая документация.
|
||
|
||
## 6. Не входит
|
||
|
||
- изменение правил автоматического discovery, группировки света и списка
|
||
исключённых интеграций — это #44;
|
||
- управление HA registry: активация, деактивация, переименование или смена area;
|
||
- автоматический перенос marker при смене HA area — это #126;
|
||
- исправление отсутствующих каналов конкретного multi-channel устройства — #109;
|
||
- bulk hide/show/delete/add;
|
||
- история устройств, графики и статистика;
|
||
- добавление виртуальных markers в lifecycle-каталог: они не имеют HA binding и
|
||
остаются редактируемыми кликом на плане;
|
||
- гарантированная полнофункциональность редактора на touch по
|
||
`docs/TOUCH-SUPPORT.md`.
|
||
|
||
## 7. Термины и две независимые оси
|
||
|
||
### 7.1 Exact binding
|
||
|
||
Ключ строки — точная привязка `device:<device_id>` либо
|
||
`entity:<entity_id>`. `device:D` и `entity:X`, принадлежащая D, являются разными
|
||
ключами и могут осознанно сосуществовать. Семейство HA device не является
|
||
единицей дедупликации.
|
||
|
||
### 7.2 Базовая категория — пользовательское намерение
|
||
|
||
| Категория | Условие | Смысл |
|
||
|---|---|---|
|
||
| `on_plan` — «На плане» | Binding представлена runtime-устройством на плане либо живым marker с `hidden !== true` | Объект уже принадлежит плану. Отсутствие сохранённого marker не делает автоустройство «не добавленным» |
|
||
| `available` — «Доступны» | Активная допустимая binding не представлена runtime-устройством, живым marker или tombstone | Её можно добавить впервые |
|
||
| `hidden` — «Скрытые» | Живой marker имеет `hidden:true` | Объект сохранён, но пользовательское намерение — не показывать его |
|
||
| `readd` — «Доступны снова» | Exact binding имеет `removed:true` и её существование сейчас подтверждено | Объект был удалён с плана и может быть создан заново |
|
||
|
||
Приоритет категорий для одного exact key:
|
||
|
||
`removed → hidden → on_plan → available`.
|
||
|
||
Живой exact marker перекрывает tombstone той же exact binding. Tombstone
|
||
родителя `device:D` не перекрывает живую `entity:X`, и наоборот, согласно #262.
|
||
|
||
### 7.3 Операционный статус — состояние Home Assistant
|
||
|
||
Статус накладывается на категорию и не переносит строку между вкладками:
|
||
|
||
| Статус | Источник | Поведение |
|
||
|---|---|---|
|
||
| `active` | `HaBindingStatus.kind === 'active'` либо положительное доказательство для нового кандидата | Доступны обычные действия категории |
|
||
| `ha_disabled` | Авторитетный registry подтверждает деактивацию | Строка помечена «Отключено в Home Assistant»; показать, добавить и выполнить Find нельзя |
|
||
| `orphaned` | Авторитетный registry подтверждает отсутствие сохранённой binding | Строка сохраняет свою базовую категорию, получает предупреждение и безопасные действия Edit/Delete |
|
||
| `unverified` | Registry недоступен или ограничен | Нельзя делать отрицательный вывод; строка не объявляется удалённой или деактивированной |
|
||
|
||
Пример: сохранённый видимый marker, который позже деактивировали в HA, остаётся
|
||
в «На плане», но строка сообщает, что сейчас он временно не показывается.
|
||
`hidden:true` + HA-disabled остаётся в «Скрытые» и показывает обе причины.
|
||
|
||
## 8. Состав каталога
|
||
|
||
Pure resolver строит детерминированный snapshot из:
|
||
|
||
- runtime `_devices`;
|
||
- `config.markers`;
|
||
- `settings.new_device_ids`;
|
||
- авторитетного или ограниченного HA registry snapshot;
|
||
- live states и текущей проекции доступности;
|
||
- действующих product filters и synthetic light groups;
|
||
- связей HA area → пространство/комната.
|
||
|
||
Кандидаты собираются из того же источника и по тем же eligibility-правилам,
|
||
что текущий Add picker. Нельзя поддерживать отдельную, постепенно расходящуюся
|
||
копию правил.
|
||
|
||
Виртуальные markers исключаются. Tombstone без положительного доказательства,
|
||
что exact binding снова существует, не показывается как «Доступно снова».
|
||
Limited registry никогда не превращает неизвестность в `ha_disabled`,
|
||
`orphaned` или `readd`.
|
||
|
||
### 8.1 Entity-уровень
|
||
|
||
- Уже размещённые `entity:*` markers всегда присутствуют в каталоге.
|
||
- В категории «Доступны» individual child entities показываются после включения
|
||
фильтра **«Показывать сущности»**, как в текущем Add dialog.
|
||
- Поиск выполняется по полному индексу и может найти exact entity независимо от
|
||
её позиции после первых 200 элементов; hard cap до фильтрации запрещён.
|
||
- Явные `device:D` и `entity:X` не скрывают друг друга из каталога. Residual
|
||
automatic parent продолжает строиться по текущему контракту `buildDevices`.
|
||
|
||
### 8.2 Признак «Новое»
|
||
|
||
`settings.new_device_ids` остаётся единственным серверным источником этого
|
||
признака.
|
||
|
||
- Автоматически появившееся и уже видимое устройство находится в «На плане» и
|
||
получает badge/фильтр **«Новое»**.
|
||
- Открытие каталога и действие Find не подтверждают новизну.
|
||
- Открытие существующего диалога настройки через Edit подтверждает новизну по
|
||
текущему контракту.
|
||
- Первично отфильтрованный скрытый кандидат не получает badge, как и сегодня.
|
||
|
||
## 9. Причины и объяснения
|
||
|
||
Строка может иметь одну основную lifecycle-причину и дополнительные статусы.
|
||
В UI используются локализованные сообщения, не внутренние regex или raw id.
|
||
|
||
Stable reason enum:
|
||
|
||
- `visible_auto` — найдено автоматически и уже находится на плане;
|
||
- `visible_explicit` — добавлено или настроено явно;
|
||
- `manual_hidden` — скрыто пользователем;
|
||
- `automatic_hidden` — скрыто автоматически, точная историческая причина не
|
||
доказуема;
|
||
- `service_entry`;
|
||
- `excluded_integration`;
|
||
- `excluded_domain`;
|
||
- `grouped_light` / `represented_by_parent`;
|
||
- `removed`;
|
||
- `no_bound_room` — доступно, но HA area не связана с комнатой плана;
|
||
- `ha_disabled_device`, `ha_disabled_entity`, `ha_disabled_all_entities`;
|
||
- `orphaned_device`, `orphaned_entity`;
|
||
- `registry_unavailable`.
|
||
|
||
`duplicate_name_area` удаляется из старого ТЗ: совпадающие имена сейчас
|
||
нумеруются, а не скрываются.
|
||
|
||
Если точную причину старого seed-marker нельзя доказать по текущему registry и
|
||
правилам, показывается честное **«Скрыто автоматически»**. Новое поле с
|
||
исторической причиной в config не добавляется.
|
||
|
||
Raw binding и диагностические детали допускаются в раскрываемой строке
|
||
«Технические сведения» и копируются отдельно; они не заменяют пользовательское
|
||
объяснение.
|
||
|
||
## 10. UX
|
||
|
||
### 10.1 Панель редактора
|
||
|
||
- Кнопки **«Добавить»** и **«Скрытые и деактивированные»** заменяются одной
|
||
кнопкой **«Устройства»** с иконкой списка/устройств.
|
||
- **«Правила иконок»** остаётся отдельной кнопкой.
|
||
- Существующий локальный режим показа скрытых и HA-disabled markers призраками
|
||
на плане сохраняется, включая их реальную позицию, выбор, настройку и
|
||
перетаскивание. Его переключатель переносится с основной панели внутрь
|
||
каталога; это не persisted-настройка и не меняет `marker.hidden`.
|
||
- После закрытия каталога выбранный локальный режим действует до выхода из
|
||
редактора устройств, как сегодня. При повторном входе в редактор он выключен;
|
||
кнопка **«Устройства»** показывает активное состояние, чтобы режим не был
|
||
скрыт от пользователя.
|
||
|
||
### 10.2 Диалог
|
||
|
||
Используется wide `hp-dialog` с заголовком **«Устройства на плане»**.
|
||
|
||
Верхняя область:
|
||
|
||
1. поле поиска с initial focus;
|
||
2. действие **«Добавить виртуальное устройство»**;
|
||
3. вкладки/фильтры со счётчиками:
|
||
- **На плане**;
|
||
- **Доступны**;
|
||
- **Скрытые**;
|
||
- **Доступны снова**;
|
||
4. дополнительный фильтр **«Новые»** для «На плане»;
|
||
5. **«Показывать сущности»** для «Доступны»;
|
||
6. локальный switch **«Показывать скрытые на плане»**. Он доступен во всех
|
||
вкладках, включает существующий ghost-render скрытых и HA-disabled markers и
|
||
ничего не сохраняет в config/layout сам по себе.
|
||
|
||
Строка содержит:
|
||
|
||
- текущую иконку;
|
||
- пользовательское имя;
|
||
- тип «Устройство»/«Сущность»;
|
||
- пространство и комнату, если известны;
|
||
- интеграцию/модель, если известны;
|
||
- badge «Новое» и operational status;
|
||
- одну короткую причину;
|
||
- primary action и меню дополнительных действий.
|
||
|
||
Поиск регистронезависим и работает по имени, модели, интеграции, названию
|
||
пространства/комнаты, `entity_id` и exact binding. Сначала фильтруется полный
|
||
snapshot, затем применяется progressive rendering. Первые 100 строк выводятся
|
||
сразу, остальные — кнопкой «Показать ещё» порциями по 100; счётчики всегда
|
||
относятся к полному результату.
|
||
|
||
Пустое состояние каждой вкладки объясняет, что сюда попадает и какое действие
|
||
может изменить результат.
|
||
|
||
### 10.3 Действия строк
|
||
|
||
| Категория/статус | Primary | Дополнительные действия |
|
||
|---|---|---|
|
||
| На плане, реально отображается | **Найти на плане** | Настроить, Скрыть |
|
||
| На плане, временно не отображается из-за HA status | **Настроить** | Найти призрак при включённом ghost-режиме; Открыть в HA; Удалить через существующий dialog |
|
||
| Скрытые, active | **Показать** | Найти призрак при включённом ghost-режиме; Настроить |
|
||
| Скрытые, HA-disabled | **Настроить** | Найти призрак при включённом ghost-режиме; Открыть в HA; «Показать» disabled с объяснением |
|
||
| Доступны | **Добавить** | Скрыть из списка |
|
||
| Доступны снова | **Добавить заново** | — |
|
||
|
||
Hide/Show — обратимые одиночные действия без подтверждения, как текущий флаг
|
||
скрытия. Delete остаётся только в существующем диалоге и сохраняет текущее
|
||
подтверждение. Add/Re-add не сохраняют объект немедленно: они открывают
|
||
существующий device dialog с preselected exact binding; Cancel ничего не пишет.
|
||
|
||
«Скрыть из списка» материализует обычный `hidden:true` exact marker и переносит
|
||
строку в «Скрытые». Это не tombstone и всегда обратимо.
|
||
|
||
### 10.4 Find и возврат из настройки
|
||
|
||
Find:
|
||
|
||
- доступен для обычного marker, если тот реально отрисовывается, и для
|
||
hidden/HA-disabled marker, если включён локальный ghost-режим; в противном
|
||
случае действие disabled и объясняет, что сначала надо включить
|
||
**«Показывать скрытые на плане»**;
|
||
- переключает пространство при необходимости;
|
||
- закрывает каталог;
|
||
- перемещает viewport так, чтобы marker оказался внутри безопасной центральной
|
||
области, не меняя zoom без необходимости;
|
||
- выделяет marker существующим selection style и дополнительным конечным
|
||
акцентом не дольше 1,5 секунды;
|
||
- не открывает карточку, не выполняет tap action и не подтверждает badge «Новое».
|
||
|
||
Add/Edit/Show через существующий device dialog временно заменяют каталог.
|
||
Cancel или Save возвращают пользователя в тот же tab/search/filter и к той же
|
||
логической строке; snapshot пересчитывается. После удаления открывается
|
||
обновлённая категория «Доступны снова», если binding подтверждена HA.
|
||
|
||
Registry/config refresh при открытом каталоге обновляет строки и счётчики, но
|
||
не сбрасывает tab/search/filter. Открытый device dialog не закрывается из-за
|
||
refresh; результат применяется после возврата.
|
||
|
||
## 11. Модель данных, сохранение и конкуренция
|
||
|
||
Новых persisted-полей и версии модели нет.
|
||
|
||
- Категория, статус, reason и состояние UI вычисляются в памяти.
|
||
- Tab, search, entity/new filters, progressive limit, scroll anchor и
|
||
`returnToInbox` — локальное состояние текущего экземпляра карточки и не
|
||
переживают reload.
|
||
- Hide/Show используют существующий `marker.hidden`.
|
||
- Delete/Re-add используют существующий `marker.removed` и текущую очистку
|
||
layout/files/trails.
|
||
- New badge использует `settings.new_device_ids` без смены формата.
|
||
- Любая mutation проходит существующий config revision/expected-rev путь.
|
||
Конфликт не создаёт дубликат: карточка перечитывает snapshot, сохраняет
|
||
tab/search и показывает локализованный toast.
|
||
- Открытие каталога, Find, поиск, смена вкладки, раскрытие причины и Show more не
|
||
вызывают `_saveConfig`, layout update или acknowledgement.
|
||
|
||
Существующий `filter_seeded` остаётся compatibility-механизмом. Если старый
|
||
config материализуется при входе редактирующего клиента, это отдельная
|
||
идемпотентная миграция текущего продукта; каталог не инициирует и не маскирует
|
||
её как пользовательское действие.
|
||
|
||
## 12. Архитектурный контракт
|
||
|
||
Новый pure-модуль (рабочее имя `src/device-inbox.ts`) владеет:
|
||
|
||
- `DeviceInboxRow`, category/status/reason enums;
|
||
- сборкой и дедупликацией exact bindings;
|
||
- capability flags действий;
|
||
- поисковым индексом и стабильной сортировкой.
|
||
|
||
Он не импортирует Lit, не пишет config и не выполняет service calls.
|
||
|
||
Eligibility новых bindings обязана использовать общий helper текущего Add
|
||
picker. В ходе реализации общая логика извлекается из
|
||
`houseplan-card.ts::_bindingCandidates`; две независимые реализации запрещены.
|
||
|
||
Стабильная сортировка внутри вкладки:
|
||
|
||
1. `new`;
|
||
2. blocking operational status;
|
||
3. localized display name;
|
||
4. exact binding как tie-breaker.
|
||
|
||
Runtime complexity: O(devices + entities + markers) на snapshot плюс O(rows)
|
||
на поиск. Нельзя для каждой строки повторно обходить все registry entities.
|
||
Результат memoize по config epoch, registry revision, device roster signature,
|
||
new ids и UI entity filter.
|
||
|
||
## 13. Доступность и responsive
|
||
|
||
- `hp-dialog` сохраняет focus trap и restore focus.
|
||
- Search получает initial focus; Escape закрывает dialog.
|
||
- Tabs имеют `role=tablist`, выбранная вкладка — `aria-selected`; стрелки
|
||
переключают вкладки.
|
||
- Строка и каждое действие достижимы Tab; icon-only действия имеют aria-label.
|
||
- Изменение результатов поиска объявляется ненавязчивым `aria-live=polite`.
|
||
- На узкой ширине метаданные переносятся под имя, действия не создают
|
||
горизонтальный скролл.
|
||
- View/киоск не меняются. Редактор на touch остаётся best effort; каталог не
|
||
должен ломать pinch/pan View и не добавляет gestures на сам план.
|
||
|
||
## 14. i18n
|
||
|
||
Новые ключи создаются одновременно в `src/i18n/en.json` и
|
||
`src/i18n/ru.json` для:
|
||
|
||
- кнопки и заголовка каталога;
|
||
- четырёх вкладок, счётчиков и empty states;
|
||
- search, Show entities, New, Show more;
|
||
- Add virtual, Find, Edit, Hide, Show, Hide from list, Add, Re-add;
|
||
- всех reason/status из §9;
|
||
- unavailable action hints, refresh/conflict toast и accessibility labels.
|
||
|
||
Существующий `marker.hide_tip` в обоих языках обновляется: вместо удаляемой
|
||
кнопки **«Скрытые и деактивированные»** он направляет в каталог
|
||
**«Устройства»**. Перед реализацией выполняется поиск остальных ссылок на старые
|
||
названия кнопок; пользовательские строки не должны вести к отсутствующему UI.
|
||
|
||
Имена integration/model/binding не переводятся. Формулировки интерфейса
|
||
синхронизируются с RU/EN user guide.
|
||
|
||
## 15. Критерии приёмки
|
||
|
||
### AC1 — единая точка входа (golden + smoke)
|
||
|
||
В Device editor вместо двух кнопок Add/Show hidden отображается одна кнопка
|
||
«Устройства»; она открывает каталог с четырьмя категориями, поиском и
|
||
счётчиками. Локальный ghost-toggle доступен внутри каталога, а его активность
|
||
видна на кнопке «Устройства». «Правила иконок» остаётся доступной.
|
||
|
||
### AC2 — детерминированная классификация (unit)
|
||
|
||
Матрица покрывает auto no-marker, explicit visible, manual/automatic hidden,
|
||
HA-disabled, orphaned, unverified, removed active, removed missing,
|
||
device-parent tombstone + live entity child, synthetic light group и candidate
|
||
без bound room. Каждая включённая exact binding находится ровно в одной базовой
|
||
категории.
|
||
|
||
### AC3 — принятое правило auto/new (unit + smoke)
|
||
|
||
Автообнаруженное уже видимое устройство без marker находится в «На плане» и
|
||
получает «Новое», если его runtime id есть в `new_device_ids`. Открытие каталога
|
||
и Find badge не снимают; Edit снимает по текущему контракту.
|
||
|
||
### AC4 — lifecycle и HA status независимы (unit)
|
||
|
||
`hidden:true + ha_disabled` остаётся в «Скрытые» с двумя объяснениями;
|
||
`hidden:false + ha_disabled` остаётся в «На плане»: Show недоступен, а Find
|
||
доступен только для призрака при включённом ghost-режиме.
|
||
Limited registry не создаёт ложный disabled/orphaned/readd.
|
||
|
||
### AC5 — exact binding и повторное добавление (unit + smoke)
|
||
|
||
`device:D` tombstone и живой `entity:X` образуют две корректные строки.
|
||
Re-add exact binding заменяет только её tombstone, не создаёт дубликат, не
|
||
возвращает старую позицию/файлы/trail и сохраняет соседний parent/child lifecycle.
|
||
|
||
### AC6 — действия используют существующие транзакции (smoke)
|
||
|
||
Find, Edit, Hide, Show, Add, Hide from list и Re-add дают результат §10.3.
|
||
Cancel из Add/Re-add не меняет данные; Delete остаётся подтверждаемым действием
|
||
существующего dialog.
|
||
|
||
Переключатель ghost-режима внутри каталога воспроизводит текущий локальный показ
|
||
hidden и HA-disabled markers: после закрытия каталога их можно найти, выбрать,
|
||
настроить и перетащить без изменения `marker.hidden`; выход из редактора
|
||
сбрасывает режим.
|
||
|
||
### AC7 — read-only открытие (unit + smoke)
|
||
|
||
Snapshot config, layout и обе revisions до/после открытия, поиска, tab switch,
|
||
reason expand, Show more и Find идентичны. Ни один из этих путей не вызывает
|
||
config/layout websocket write.
|
||
|
||
### AC8 — возврат и live refresh (smoke)
|
||
|
||
После Edit/Save/Cancel каталог возвращается с прежними tab/search/filter и
|
||
логическим scroll anchor. Registry/config refresh пересчитывает строку без
|
||
закрытия открытого device dialog и без дубликатов.
|
||
|
||
### AC9 — поиск и большие реестры (unit + smoke)
|
||
|
||
Поиск находит строку по каждому полю §10.2, включая entity за пределом первых
|
||
200 исходных записей. Progressive rendering не меняет total counts и не
|
||
приводит к вложенному полному обходу registry на каждую строку.
|
||
|
||
### AC10 — accessibility/responsive (smoke + golden + review)
|
||
|
||
Диалог работает с клавиатуры, восстанавливает focus, не имеет горизонтального
|
||
скролла на narrow fixture и корректен в RU/EN и light/dark. Touch View и kiosk
|
||
не получают новых интерактивных слоёв.
|
||
|
||
### AC11 — compatibility (unit + existing regressions)
|
||
|
||
Текущие тесты `devices`, `ha-binding-status`, `device-presentation`,
|
||
`binding_picker` и `hidden_flag` сохраняют смысл. Старые markers/settings читаются
|
||
без миграции, а каталог не меняет auto-placement, filtering, light aggregation,
|
||
LQI/climate или opening references.
|
||
|
||
## 16. План автотестов
|
||
|
||
- `test/device-inbox.test.mjs` — pure resolver, reason/capability/search/sort,
|
||
large registry и exact-binding matrix;
|
||
- `test/devices.test.mjs`, `test/ha-binding-status.test.mjs` — соседние
|
||
регрессии lifecycle;
|
||
- `demo/smoke_device_inbox.mjs` — реальный dialog, read-only assertions,
|
||
row actions, return state, late registry refresh;
|
||
- адаптация `demo/smoke_binding_picker.mjs` и `demo/smoke_hidden_flag.mjs` без
|
||
потери существующих проверок: `smoke_hidden_flag` включает ghost-режим через
|
||
каталог и по-прежнему проверяет позицию, клик/настройку и drag скрытого marker;
|
||
- golden: desktop RU/EN, light/dark; narrow RU с длинными причинами;
|
||
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` определяет
|
||
дополнительный набор перед код-ревью.
|
||
|
||
Минимальные локальные гейты реализации:
|
||
|
||
```text
|
||
npx tsc --noEmit
|
||
npm test
|
||
npm run bundle:sync
|
||
node scripts/check-docs.mjs
|
||
node scripts/smoke-select.mjs --base origin/dev --head HEAD
|
||
node demo/smoke_device_inbox.mjs
|
||
node demo/smoke_binding_picker.mjs
|
||
node demo/smoke_hidden_flag.mjs
|
||
npm run golden:verify
|
||
```
|
||
|
||
## 17. Производительность
|
||
|
||
- полный индекс может включать тысячи entity, но DOM получает не более 100
|
||
строк за одну порцию;
|
||
- поиск и counts считаются по полному memoized snapshot;
|
||
- registry refresh инвалидирует snapshot один раз на revision;
|
||
- новый код не добавляет рендеров или observers в View/киоск;
|
||
- performance benchmark обязателен только если `smoke-select` либо ревью
|
||
выявят затронутый бюджет; synthetic unit на большой registry остаётся
|
||
обязательным.
|
||
|
||
## 18. Риски
|
||
|
||
| Риск | Защита |
|
||
|---|---|
|
||
| Auto no-marker ошибочно показан как не добавленный | Effective runtime presence важнее наличия marker; AC3 |
|
||
| Device/entity parent-child дедуплицируются слишком широко | Exact binding key и fixture #262; AC5 |
|
||
| Limited registry создаёт ложные причины | Только positive evidence; AC4 |
|
||
| Catalog и Add picker расходятся | Один общий eligibility helper |
|
||
| Открытие UI материализует данные | Явный no-write contract и websocket assertions; AC7 |
|
||
| Историческая причина скрытия выдумывается | Generic `automatic_hidden` fallback |
|
||
| Большой registry блокирует dialog | O(N) snapshot + progressive rendering |
|
||
| Возврат из nested flow теряет контекст | Локальный `returnToInbox` snapshot; AC8 |
|
||
| Перенос ghost-toggle делает активный режим незаметным после закрытия каталога | Активное состояние кнопки «Устройства» и сброс при выходе из редактора; AC6 |
|
||
| #29 поглощает redesign фильтров | #44 явно вне скоупа |
|
||
|
||
## 19. Откат
|
||
|
||
Изменение не мигрирует config и не создаёт новых persisted-полей. Откат
|
||
возвращает две прежние кнопки и Add picker; markers, tombstones, hidden flags,
|
||
layout и attachments остаются совместимыми. Созданный через «Скрыть из списка»
|
||
`hidden:true` marker уже поддерживается старым UI и может быть показан обычным
|
||
способом.
|
||
|
||
## 20. Release-артефакты
|
||
|
||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #29;
|
||
- раздел Device editor в `docs/USER-GUIDE.md` и
|
||
`docs/USER-GUIDE.ru.md`;
|
||
- `docs/FILTERING.md` — каталог как единая presentation surface, без изменения
|
||
lifecycle semantics;
|
||
- `docs/ARCHITECTURE.md` — pure resolver и shared eligibility helper;
|
||
- актуальные golden baselines и публичный screenshot редактора устройств, если
|
||
документационный capture затронут;
|
||
- backend, security и model migration release notes: **не требуются**.
|
||
|
||
## 21. Принятые технические предположения — ревьюер может менять свободно
|
||
|
||
1. Pure resolver живёт в `src/device-inbox.ts`, а не внутри Lit-компонента.
|
||
2. Existing Add eligibility извлекается в общий helper; имя модуля не является
|
||
продуктовым контрактом.
|
||
3. Tab/search/filter/scroll state хранится только в экземпляре карточки.
|
||
4. Progressive page = 100 строк; конкретное число можно скорректировать по
|
||
golden/performance без изменения пользовательской семантики.
|
||
5. Точная историческая причина seed не сохраняется; при отсутствии доказательства
|
||
используется `automatic_hidden`.
|
||
6. #44 расширит тот же dialog advanced-настройками позже, но #29 не ждёт #44 и
|
||
не меняет discovery settings.
|