From fcf30073e98bd8c345f5266870a9cb2e2e85db4e Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 28 Aug 2026 00:52:11 +0300 Subject: [PATCH] docs: update device inbox lifecycle spec Issue: #29 User-Visible: no --- docs/specs/029-device-inbox-lifecycle.md | 563 +++++++++++++++++++++-- 1 file changed, 516 insertions(+), 47 deletions(-) diff --git a/docs/specs/029-device-inbox-lifecycle.md b/docs/specs/029-device-inbox-lifecycle.md index f365e6d9..6c7d82a4 100644 --- a/docs/specs/029-device-inbox-lifecycle.md +++ b/docs/specs/029-device-inbox-lifecycle.md @@ -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:` либо +`entity:`. `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.