mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
@@ -1,65 +1,534 @@
|
||||
# ТЗ #29 — Inbox и объяснимый жизненный цикл устройств
|
||||
# ТЗ #29 — единый каталог и жизненный цикл устройств
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/29
|
||||
- Приоритет: P1
|
||||
- Статус ТЗ: draft, требуется UX-утверждение
|
||||
- Связано: #44 переносит advanced-фильтры в этот интерфейс
|
||||
- Тип: feature
|
||||
- Трек: обычный
|
||||
- Связано: #44 (настройки discovery), #109 (поиск каналов устройства),
|
||||
#126 (смена HA area), #262 (возврат exact entity после удаления device)
|
||||
|
||||
## Цель
|
||||
## 1. Сценарий
|
||||
|
||||
Заменить разрозненные «Добавить»/«Показать скрытые» одним read-only при
|
||||
открытии каталогом, который объясняет состояние каждой HA-привязки и даёт
|
||||
явные действия жизненного цикла.
|
||||
**Персона:** администратор дома из `docs/SCOPE.md`.
|
||||
|
||||
## Единая классификация
|
||||
**Поверхность:** полный `houseplan-card`, desktop-first редактор устройств.
|
||||
|
||||
Pure resolver получает registry snapshot, live states, markers и product
|
||||
filters и возвращает одну запись на canonical binding:
|
||||
**Момент:** пользователь добавил, скрыл или удалил устройства, 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 Базовая категория — пользовательское намерение
|
||||
|
||||
| Категория | Условие | Смысл |
|
||||
|---|---|---|
|
||||
| Новые | активная допустимая binding без marker/tombstone | Добавить, скрыть |
|
||||
| На плане | live marker, `hidden !== true` | Найти, редактировать |
|
||||
| Скрытые | live marker `hidden:true` либо seed-кандидат filter | Показать/добавить, причина |
|
||||
| Доступные снова | tombstone `removed:true`, binding снова существует | Добавить заново |
|
||||
| `on_plan` — «На плане» | Binding представлена runtime-устройством на плане либо живым marker с `hidden !== true` | Объект уже принадлежит плану. Отсутствие сохранённого marker не делает автоустройство «не добавленным» |
|
||||
| `available` — «Доступны» | Активная допустимая binding не представлена runtime-устройством, живым marker или tombstone | Её можно добавить впервые |
|
||||
| `hidden` — «Скрытые» | Живой marker имеет `hidden:true` | Объект сохранён, но пользовательское намерение — не показывать его |
|
||||
| `readd` — «Доступны снова» | Exact binding имеет `removed:true` и её существование сейчас подтверждено | Объект был удалён с плана и может быть создан заново |
|
||||
|
||||
HA-disabled binding остаётся в своём lifecycle-разделе, но получает статус
|
||||
«Отключено в Home Assistant» и недоступное действие показа согласно текущему
|
||||
контракту disabled devices. Orphaned сохранённый marker остаётся «На плане» или
|
||||
«Скрытые» с предупреждением, а не превращается в «Новый».
|
||||
Приоритет категорий для одного exact key:
|
||||
|
||||
## Причины
|
||||
`removed → hidden → on_plan → available`.
|
||||
|
||||
Причина — stable enum, локализованный в UI: `manual_hidden`, `ha_disabled`,
|
||||
`service_entry`, `excluded_integration`, `excluded_domain`, `grouped_light`,
|
||||
`represented_by_parent`, `duplicate_name_area`, `removed`, `orphaned`,
|
||||
`limited_registry`. Regex/id могут быть в раскрываемой диагностике, но не в
|
||||
основной фразе.
|
||||
Живой exact marker перекрывает tombstone той же exact binding. Tombstone
|
||||
родителя `device:D` не перекрывает живую `entity:X`, и наоборот, согласно #262.
|
||||
|
||||
## UX
|
||||
### 7.3 Операционный статус — состояние Home Assistant
|
||||
|
||||
- Кнопка редактора устройств открывает wide `hp-dialog`/side sheet с поиском,
|
||||
tabs/filters и счётчиками.
|
||||
- Просмотр, поиск и раскрытие причины ничего не пишут в config.
|
||||
- «Найти» переключает пространство, закрывает inbox и мягко выделяет marker.
|
||||
- «Добавить» открывает существующий device dialog с preselected binding.
|
||||
- «Скрыть» материализует `hidden:true`, но не `removed:true`.
|
||||
- «Добавить заново» заменяет tombstone одним live marker и не наследует старую
|
||||
позицию, файлы или trail, уже удалённые подтверждённым Delete.
|
||||
- Все mutation actions получают Undo/confirmation согласно их текущей
|
||||
семантике; bulk actions в v1 не входят.
|
||||
Статус накладывается на категорию и не переносит строку между вкладками:
|
||||
|
||||
## Инварианты и конкуренция
|
||||
| Статус | Источник | Поведение |
|
||||
|---|---|---|
|
||||
| `active` | `HaBindingStatus.kind === 'active'` либо положительное доказательство для нового кандидата | Доступны обычные действия категории |
|
||||
| `ha_disabled` | Авторитетный registry подтверждает деактивацию | Строка помечена «Отключено в Home Assistant»; показать, добавить и выполнить Find нельзя |
|
||||
| `orphaned` | Авторитетный registry подтверждает отсутствие сохранённой binding | Строка сохраняет свою базовую категорию, получает предупреждение и безопасные действия Edit/Delete |
|
||||
| `unverified` | Registry недоступен или ограничен | Нельзя делать отрицательный вывод; строка не объявляется удалённой или деактивированной |
|
||||
|
||||
Canonical binding уникальна. Повторный save и конфликт revision не создают
|
||||
дубликат. Registry refresh обновляет список с сохранением tab/search, но не
|
||||
закрывает редактируемый dialog. Limited registry не выводит ложный «удалён».
|
||||
Пример: сохранённый видимый marker, который позже деактивировали в HA, остаётся
|
||||
в «На плане», но строка сообщает, что сейчас он временно не показывается.
|
||||
`hidden:true` + HA-disabled остаётся в «Скрытые» и показывает обе причины.
|
||||
|
||||
## Проверки и приёмка
|
||||
## 8. Состав каталога
|
||||
|
||||
- матрица resolver по marker/hidden/removed/disabled/orphaned/filter;
|
||||
- два клиента и revision conflict;
|
||||
- re-add device/entity tombstones, virtual marker вне inbox;
|
||||
- поиск, keyboard navigation, narrow layout и ru/en golden;
|
||||
- любой кандидат находится ровно в одном разделе и имеет понятную причину;
|
||||
- открытие inbox не меняет config/layout/revisions.
|
||||
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 Панель редактора
|
||||
|
||||
- Кнопки **«Добавить»** и **«Скрытые и деактивированные»** заменяются одной
|
||||
кнопкой **«Устройства»** с иконкой списка/устройств.
|
||||
- **«Правила иконок»** остаётся отдельной кнопкой.
|
||||
- Скрытые markers больше не рисуются поверх плана постоянным локальным режимом:
|
||||
доступ к ним даёт каталог. Это устраняет состояние панели, неочевидное после
|
||||
возврата в редактор.
|
||||
|
||||
### 10.2 Диалог
|
||||
|
||||
Используется wide `hp-dialog` с заголовком **«Устройства на плане»**.
|
||||
|
||||
Верхняя область:
|
||||
|
||||
1. поле поиска с initial focus;
|
||||
2. действие **«Добавить виртуальное устройство»**;
|
||||
3. вкладки/фильтры со счётчиками:
|
||||
- **На плане**;
|
||||
- **Доступны**;
|
||||
- **Скрытые**;
|
||||
- **Доступны снова**;
|
||||
4. дополнительный фильтр **«Новые»** для «На плане»;
|
||||
5. **«Показывать сущности»** для «Доступны».
|
||||
|
||||
Строка содержит:
|
||||
|
||||
- текущую иконку;
|
||||
- пользовательское имя;
|
||||
- тип «Устройство»/«Сущность»;
|
||||
- пространство и комнату, если известны;
|
||||
- интеграцию/модель, если известны;
|
||||
- badge «Новое» и operational status;
|
||||
- одну короткую причину;
|
||||
- primary action и меню дополнительных действий.
|
||||
|
||||
Поиск регистронезависим и работает по имени, модели, интеграции, названию
|
||||
пространства/комнаты, `entity_id` и exact binding. Сначала фильтруется полный
|
||||
snapshot, затем применяется progressive rendering. Первые 100 строк выводятся
|
||||
сразу, остальные — кнопкой «Показать ещё» порциями по 100; счётчики всегда
|
||||
относятся к полному результату.
|
||||
|
||||
Пустое состояние каждой вкладки объясняет, что сюда попадает и какое действие
|
||||
может изменить результат.
|
||||
|
||||
### 10.3 Действия строк
|
||||
|
||||
| Категория/статус | Primary | Дополнительные действия |
|
||||
|---|---|---|
|
||||
| На плане, реально отображается | **Найти на плане** | Настроить, Скрыть |
|
||||
| На плане, временно не отображается из-за HA status | **Настроить** | Открыть в HA, Удалить через существующий dialog |
|
||||
| Скрытые, active | **Показать** | Настроить |
|
||||
| Скрытые, HA-disabled | **Настроить** | Открыть в HA; «Показать» disabled с объяснением |
|
||||
| Доступны | **Добавить** | Скрыть из списка |
|
||||
| Доступны снова | **Добавить заново** | — |
|
||||
|
||||
Hide/Show — обратимые одиночные действия без подтверждения, как текущий флаг
|
||||
скрытия. Delete остаётся только в существующем диалоге и сохраняет текущее
|
||||
подтверждение. Add/Re-add не сохраняют объект немедленно: они открывают
|
||||
существующий device dialog с preselected exact binding; Cancel ничего не пишет.
|
||||
|
||||
«Скрыть из списка» материализует обычный `hidden:true` exact marker и переносит
|
||||
строку в «Скрытые». Это не tombstone и всегда обратимо.
|
||||
|
||||
### 10.4 Find и возврат из настройки
|
||||
|
||||
Find:
|
||||
|
||||
- доступен только если marker реально отрисовывается;
|
||||
- переключает пространство при необходимости;
|
||||
- закрывает каталог;
|
||||
- перемещает 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.
|
||||
|
||||
Имена integration/model/binding не переводятся. Формулировки интерфейса
|
||||
синхронизируются с RU/EN user guide.
|
||||
|
||||
## 15. Критерии приёмки
|
||||
|
||||
### AC1 — единая точка входа (golden + smoke)
|
||||
|
||||
В Device editor вместо двух кнопок Add/Show hidden отображается одна кнопка
|
||||
«Устройства»; она открывает каталог с четырьмя категориями, поиском и
|
||||
счётчиками. «Правила иконок» остаётся доступной.
|
||||
|
||||
### 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 недоступны.
|
||||
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.
|
||||
|
||||
### 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` без
|
||||
потери существующих проверок;
|
||||
- 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 |
|
||||
| #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.
|
||||
|
||||
Reference in New Issue
Block a user