diff --git a/docs/specs/054-zigbee-topology-overlay.md b/docs/specs/054-zigbee-topology-overlay.md index 98f29eb1..4b549498 100644 --- a/docs/specs/054-zigbee-topology-overlay.md +++ b/docs/specs/054-zigbee-topology-overlay.md @@ -1,74 +1,533 @@ -# ТЗ #54 — Диагностический Zigbee topology overlay +# ТЗ #54 — контекстные связи Zigbee на плане - Issue: https://github.com/Matysh/houseplan-card/issues/54 -- Приоритет: P2 -- Статус ТЗ: research + adapter contract; provider APIs проверяются до кода +- Приоритет: P3 +- Тип: feature +- Трек: полный — новый UX-контракт, провайдеры, настройки, privacy и performance +- Stage 0: capability research завершён, решение **GO с условиями** +- Контракты проверены: 2026-08-31 +- UX скорректирован решением владельца: 2026-09-04 -## Цель +## 1. Сценарий -По явному запросу показать связи ZHA/Zigbee2MQTT между уже размещёнными -устройствами поверх реальной геометрии, не превращая House Plan в редактор сети. +Персона — Enthusiast/Power User из `docs/SCOPE.md`. Поверхность — полная +карточка House Plan в режиме View на устройстве с мышью. Момент — пользователь +ищет пространственную причину нестабильной работы конкретного Zigbee-устройства +и хочет увидеть его наблюдаемых соседей относительно комнат и стен, не переходя +в отдельный сетевой граф. -## Stage 0 — capability research +## 2. Что человек увидит до и после -На поддерживаемых версиях HA зафиксировать официальные/фактические API: +До: House Plan показывает LQI устройства и комнаты, но не показывает, с какими +Zigbee-узлами связано выбранное устройство и где они находятся на плане. -- ZHA topology/neighbour source, permissions, direction и LQI semantics; -- Zigbee2MQTT networkmap request/response через HA MQTT integration либо - документированную exposed entity; -- соответствие IEEE ↔ HA device registry `connections`; -- timeout/rate limits и поведение sleeping end devices. +После: если пользователь заранее включил диагностику в общих настройках и навёл +мышь на размещённое Zigbee-устройство, House Plan временно показывает только его +непосредственные связи; без наведения план выглядит как раньше. -Неподтверждённые command/topic не hardcode-ятся. Каждый provider имеет fixture -с обезличенными ids и дату/версию проверенного контракта. +## 3. Проблема -## Normalized model +Полный mesh-граф поверх жилого плана быстро превращается в нечитаемую паутину: +число линий растёт вместе с сетью, связи закрывают геометрию и устройства, а +пользователь всё равно диагностирует один проблемный endpoint за раз. При этом +ZHA и Zigbee2MQTT дают разные по свежести и стоимости snapshots, а их neighbor +records нельзя честно называть фактическим маршрутом каждого пакета. + +Нужен opt-in диагностический слой с progressive disclosure: сеть не видна в +обычном состоянии, а непосредственное окружение одного устройства появляется +только на время hover. + +## 4. Scope и non-scope + +### 4.1 Входит в #54 + +- одна выключенная по умолчанию опция в «Общих настройках»; +- provider-neutral topology model и точное сопоставление узлов с размещёнными + маркерами; +- ZHA cached snapshot через штатный admin-only WebSocket API; +- Zigbee2MQTT raw network map через штатные HA MQTT surfaces и явно заданные + base topics; +- ручное получение/обновление provider data из «Общих настроек»; +- только непосредственные связи наведённого устройства; +- локальные линии внутри текущего пространства и временный счётчик связей в + другие пространства; +- состояния загрузки, устаревшего snapshot, partial data, unsupported и error; +- lazy loading, общий memory cache, privacy guards, i18n, unit/backend/browser, + golden и performance coverage. + +### 4.2 Не входит в #54 + +- постоянный слой или кнопка «показать весь граф»; +- вычисление либо изображение «маршрута до координатора»; +- tap, long press или другой topology-жест на touch/pen; +- интерактивная панель cross-space links и переход к удалённому marker; +- линии между независимыми пространствами, этажами или экземплярами карточки; +- принудительный ZHA radio scan без completion-aware upstream API; +- background/periodic scan, scan по hover, HA state tick, смене пространства или + созданию второй карточки; +- pairing, reconfigure, bind/unbind, удаление устройств и изменение сети; +- автоматическое создание маркеров, matching по имени/model/friendly name; +- RF-прогноз по стенам, причинные утверждения, history и графики; +- topology в kiosk, редакторах и `houseplan-space-card`; +- доступ non-admin в обход штатных прав Home Assistant. + +## 5. Контракт поведения + +### 5.1 Общая настройка + +В «Общих настройках» появляется переключатель +«Показывать связи Zigbee при наведении на устройство». + +- missing/invalid/`false` означает выключено; +- значение сохраняется вместе с общими настройками и действует для всех + пространств полной карточки; +- выключенное состояние не загружает topology runtime, не вызывает provider API + и не строит topology/mapping; +- включение разрешает отображение, но само по себе не запускает radio scan; +- выключение немедленно убирает hover-слой. Уже начатый явный Z2M scan не + прерывается небезопасным unsubscribe: ответ может завершить shared cache, но + UI остаётся скрыт; +- non-admin не видит активный control и не инициирует provider requests, даже + если настройку ранее сохранил admin. + +### 5.2 Получение данных + +Под переключателем, только в раскрытом включённом состоянии, показываются +provider controls и статус последнего snapshot. + +- ZHA: действие «Прочитать данные ZHA» читает cached `zha/devices`. Оно не + обещает свежий radio scan и не показывает фиктивный момент сканирования; +- Zigbee2MQTT: для каждой настроенной instance задаётся base topic и доступно + действие «Обновить карту Zigbee2MQTT»; +- перед Z2M scan показывается предупреждение: операция занимает от 10 секунд до + 2 минут и временно может снизить отзывчивость сети; +- provider actions явные и независимые. Ошибка одного provider не скрывает + пригодный snapshot другого; +- статус сообщает provider, время получения House Plan, stale/partial и + человекопонятную ошибку. IEEE, raw payload и внутренние identifiers не + показываются; +- snapshot старше пяти минут помечается устаревшим, но не обновляется сам. + +### 5.3 Hover одного устройства + +Связи разрешены только если одновременно выполняются условия: + +1. настройка включена; +2. текущая поверхность — View полной карточки, не kiosk; +3. последняя pointer modality — реальная мышь с fine/hover capability; +4. существует пригодный snapshot; +5. курсор находится над drawable live marker, точно сопоставленным Zigbee node. + +Тогда House Plan берёт только incident links выбранного node — пары, в которых +этот node является одним из концов. Весь остальной граф не строится и не +рисуется. Это наблюдаемые непосредственные соседи, а не вычисленный путь до +координатора. + +Если второй endpoint точно сопоставлен видимому marker текущего пространства, +рисуется одна связь между маркерами, а marker-сосед получает лёгкое временное +выделение. Если второй endpoint находится в другом пространстве, линия не +рисуется; рядом с исходным marker появляется компактный неинтерактивный счётчик +«ещё N в других пространствах». Unmatched, ambiguous, hidden, removed и +HA-disabled endpoints не рисуются и в cross-space count не входят. + +Hover-слой очищается при pointerleave marker, переходе на touch/pen, смене +пространства или режима, remount/disconnect и выключении настройки. Он не +остаётся закреплённым после клика. Existing click, long-press, hover tooltip, +device action и dispatched-action animation не меняются. + +### 5.4 Touch, keyboard и другие режимы + +- Touch/pen topology не показывают и не получают нового жеста. Tap и long press + продолжают действовать по существующему контракту устройства. +- Keyboard focus не имитирует mouse hover и не вводит новую навигацию по плану; + это соответствует текущей accessibility-границе `docs/SCOPE.md`. +- Plan, Devices и Decor не показывают линии, соседнее выделение или cross-space + count независимо от сохранённой настройки. +- Kiosk и `houseplan-space-card` не загружают runtime и не делают запросы. +- Несколько полных карточек могут переиспользовать один snapshot, но каждая + самостоятельно владеет своим mouse-hover состоянием. + +## 6. UX и визуальная семантика + +- В обычном View при включённой функции нет постоянной легенды, badges или + линий. Визуальный шум равен выключенному состоянию. +- Связи находятся в отдельном pointer-transparent layer: над архитектурой и + decor, но под device markers и их tooltips. +- Линия соединяет фактические центры двух уже спроецированных маркеров, не + участвует в fit/bounds и не меняет геометрию плана. +- Цвет использует каноническую непрерывную шкалу House Plan LQI. Unknown LQI — + нейтральный пунктир. Provider error никогда не изображается как «плохая» + красная связь. +- Для двух directional observations визуал линии использует наблюдение от + наведённого node к соседу; обратное значение не усредняется и остаётся в + нормализованной модели для диагностики/тестов. +- Толщина/opacity допускают ограниченное усиление по LQI, но сохраняются в + читаемом screen-space диапазоне при любом zoom. +- Coordinator/router/end не получают постоянных новых обозначений. Во время + hover сосед может получить один нейтральный halo, не похожий на alert, + selection или dispatched-action animation. +- Cross-space count живёт только пока активен тот же hover, имеет + `pointer-events:none`, не становится частью hit area и не предлагает tap. +- При отсутствии incident links или drawable соседей план не показывает + ложную линию или toast. Причина и summary доступны в provider status общих + настроек. + +## 7. Подтверждённые provider-контракты Stage 0 + +### 7.1 ZHA + +Проверены Home Assistant Core `2026.8.3` и +`dev@6ad726ba56d517e56536cdd6fa2ba9f358bbc0ef`, Home Assistant Frontend +`dev@6b7d3871754dada4d599e4fecc0d5ca0442cf22b`, а также zigpy topology. + +1. Admin-only `zha/devices` возвращает `device_reg_id`, IEEE, роль, + `neighbors`, `routes` и directional LQI. +2. `device_reg_id` даёт exact HA Device Registry mapping. +3. Штатный HA frontend строит граф из `neighbors`/`routes`, сохраняет два + directional LQI пары и использует route только вместе с реальным neighbor + next hop. +4. zigpy обычно сканирует topology раз в четыре часа. `zha/devices` читает + кешированный snapshot, который может быть старым или неполным. +5. `zha/topology/update` в проверенном контракте запускает background scan, но + не возвращает completion/result. Поэтому #54 его не вызывает. + +### 7.2 Zigbee2MQTT + +Проверены Zigbee2MQTT `2.13.0` и +`master@fcbb7ff44bdc05a16e95d3472a81d110646cc17b`. + +1. Документированный request + `BASE/bridge/request/networkmap` с + `{"type":"raw","routes":false,"transaction":"…"}` отвечает на + `BASE/bridge/response/networkmap` и возвращает nodes/links. +2. Node содержит IEEE, friendly name, роль и scan failures. Link содержит + source, target, directional LQI и relationship. +3. Scan занимает 10 секунд — 2 минуты и может временно снизить отзывчивость + Zigbee-сети; только явное действие пользователя вправе его запускать. +4. `routes:false` достаточно для непосредственных neighbor edges и уменьшает + нагрузку. +5. HA MQTT предоставляет admin-only WebSocket subscription, а `mqtt.publish` — + штатное HA action. Браузер не подключается к broker напрямую. +6. Base topic конфигурируем и не выводится надёжно из HA registry. Для каждой + instance пользователь задаёт его явно; default `zigbee2mqtt` проверяется по + retained `bridge/info` до scan. + +## 8. Модель данных и миграция + +### 8.1 Сохраняемая настройка + +Концептуальная форма общих настроек: ```ts -type ZigbeeTopology = { - provider: 'zha' | 'z2m'; capturedAt: number; - nodes: Array<{ key: string; deviceId?: string; role: 'coordinator'|'router'|'end' }>; - links: Array<{ from: string; to: string; lqi?: number; direction: 'one'|'both'|'unknown' }>; - warnings: string[]; +type ZigbeeTopologySettings = { + enabled?: boolean; // missing/invalid => false + z2mBaseTopics?: string[]; // normalized exact base topics }; ``` -Provider adapters живут backend-side либо в изолированном HA adapter; render -не знает API. Browser не подключается к MQTT напрямую и не хранит topology в -House Plan config. +Точное внутреннее имя поля вправе изменить реализация без изменения UX. +Настройка принадлежит плану целиком, а не пространству. Повторное сохранение +несвязанных общих настроек не удаляет её. Дубликаты/пустые/невалидные base topic +нормализуются fail-closed. -## Fetch/cache/security +Миграции существующих планов нет: отсутствие объекта означает выключенную +функцию. Downgrade игнорирует неизвестное поле. Выключение функции не удаляет +base topics, чтобы повторное включение не требовало настройки заново. -- Overlay off по умолчанию; первый toggle запускает fetch с progress/cancel. -- Shared per-provider cache TTL 60 s (уточняется Stage 0), in-flight dedupe и - hard timeout. Toggle off отменяет UI ожидание, но безопасный backend request - может завершить cache. -- Refresh только явный или после TTL; HA state ticks не сканируют topology. -- Read permission проверяется backend. Ошибка/unsupported не ломает plan и - показывает localized status. +### 8.2 Runtime model -## Mapping и визуал +Provider payload не проходит в render: -Node сопоставляется marker только через device registry/explicit entity owner. -На plan рисуются links, у которых оба конца имеют видимые live markers текущего -space. Остальные nodes доступны в summary «Не размещено N», но не рисуются. +```ts +type ZigbeeProvider = 'zha' | 'z2m'; -Edges — pointer-transparent отдельный layer. Цвет/opacity/thickness отражают -нормализованный LQI только при сопоставимой шкале provider; unknown — нейтральный -пунктир. Direction optional arrow только при достоверных данных. Coordinator -и router имеют legend. Geometry стен не интерпретируется как причина сигнала. +type ZigbeeTopology = { + provider: ZigbeeProvider; + instanceId: string; + obtainedAt: number; + freshness: 'provider-cache' | 'fresh-scan'; + nodes: ZigbeeTopologyNode[]; + links: ZigbeeTopologyLink[]; + warnings: ZigbeeTopologyWarning[]; +}; -## Multiple spaces и lifecycle +type ZigbeeTopologyNode = { + key: string; // provider instance + normalized IEEE + ieee: string; // private runtime value + deviceId?: string; + role: 'coordinator' | 'router' | 'end' | 'unknown'; + available?: boolean; +}; -Cross-space link не рисуется как линия через разные планы; endpoints получают -badge/count и список другого пространства. Hidden/removed/disabled marker не -рисуется. Rebind refreshes mapping без нового network scan. +type ZigbeeDirectionalObservation = { + lqi?: number; // provider-native 0..255 + relationship?: string; + activeRoute?: boolean; +}; -## Performance и проверки +type ZigbeeTopologyLink = { + a: string; + b: string; + aToB?: ZigbeeDirectionalObservation; + bToA?: ZigbeeDirectionalObservation; +}; -- 20/100/500 nodes, dense mesh; edge cap/viewport culling после Stage 0; -- provider fixtures, timeout, malformed/cyclic/duplicate links; -- mapping IEEE/device/entity, cross-space, hidden lifecycle; -- lazy request/in-flight cache and no fetch on ordinary HA ticks; -- golden LQI/unknown legend и performance budget; -- network layer полностью исчезает при toggle off и не меняет config. +type ZigbeeTopologyWarning = { + code: + | 'invalid_payload' + | 'duplicate_link' + | 'self_link' + | 'unmatched_device' + | 'ambiguous_placement' + | 'provider_scan_failure'; + nodeKey?: string; +}; +``` + +Snapshot остаётся memory-only и не входит в экспорт, backup, diagnostics, +support report, localStorage или console. Shared cache key включает HA connection, +provider и instance; concurrent requests дедуплицируются. + +## 9. Нормализация и mapping + +- IEEE нормализуется до одного canonical representation; raw/friendly name не + используется как identity. +- Одинаковая unordered pair хранится один раз; directional observations не + усредняются и не выдумываются из отсутствующей стороны. +- Self links и exact duplicates отбрасываются с warning. +- Route destination не создаёт прямую линию: route может только пометить + существующий neighbor next hop. +- Некорректный/неизвестный LQI становится `undefined`, а не `0`. +- Payload ограничивается по bytes, nodes и links до дорогостоящей обработки. + +Для каждого node сначала определяется exact HA `deviceId`, затем marker: + +1. единственный видимый live marker с binding `device:`; +2. если его нет — единственный видимый entity marker, чья entity принадлежит + тому же HA device; +3. несколько равноправных markers дают `ambiguous_placement` и не рисуются; +4. hidden, removed и HA-disabled markers не являются drawable endpoints; +5. rebind, перемещение и смена пространства пересчитывают mapping по тому же + snapshot без provider request. + +Для Z2M обычный identifier `zigbee2mqtt_`, а bridge/coordinator — +`zigbee2mqtt_bridge_`. Exact Entity Registry owner допускается как +fallback. User-overridden identifiers могут оставить node unmatched; matching +по friendly name, entity name или model запрещён. + +## 10. Fetch, cache, lifecycle и permissions + +- Runtime загружается динамически только после сохранённого `enabled:true` на + доступной admin View/full-card поверхности. +- Provider data загружается только явными controls в общих настройках; hover + никогда не вызывает fetch или scan. +- Shared cache — per HA connection + provider + instance, memory-only, с + in-flight dedupe. Snapshot старше пяти минут stale, но пригоден до явного + обновления или конца connection lifecycle. +- Z2M transaction обязателен; timeout — 150 секунд. Retained ответ до request, + ответ с чужой transaction и late response игнорируются; subscription всегда + очищается после success/error/timeout. +- Hide/disable, изменение binding, удаление marker, смена режима/пространства и + disconnect синхронно инвалидируют draw mapping/hover, но не запускают scan. +- Provider/API failure не ломает план, device actions, LQI badges/fill и + остальные режимы. +- Если HA отказывает в admin API, adapter fail-closed и не пытается обойти права + через backend House Plan. + +## 11. Ограничения достоверности + +- Neighbor tables — наблюдаемый snapshot, а не лог текущих packet routes. +- Спящие end devices могут отсутствовать или иметь устаревшую связь. +- Пара может наблюдаться только с одной стороны; это нормальный результат. +- Coordinator/router failures могут оставить частичный граф. +- Диапазон LQI 0..255 общий, но providers не гарантируют одинаковую методику и + момент измерения. Межпровайдерный рейтинг качества не строится. +- Геометрия стен используется только как визуальный контекст. House Plan не + утверждает, что конкретная стена вызвала слабый сигнал. + +## 12. i18n + +Новые пользовательские строки добавляются синхронно в `en`, `ru`, `de`, `fr`: + +- название и help переключателя; +- названия ZHA/Z2M provider controls; +- статус «данные не загружены / получены / устарели / частичные»; +- предупреждение о длительности и нагрузке Z2M scan; +- ошибки permission, unsupported, timeout, invalid base topic/payload; +- cross-space count с pluralization; +- нейтральное summary «связи не найдены / часть узлов не показана». + +Тексты используют «наблюдаемые связи» и «полученные данные», а не «текущий +маршрут». Internal identifiers и raw provider errors локализованную границу не +пересекают. + +## 13. Критерии приёмки + +- **AC1 — default/off.** Новый и существующий план без валидного `enabled:true` + визуально и по поведению совпадает с текущим View; topology runtime/API не + вызываются. Доказательство: unit config tests + browser request spy, Codex. +- **AC2 — persistence.** Admin может включить/выключить настройку в «Общих + настройках»; она переживает reload, относится ко всем spaces и не теряется при + сохранении соседнего поля. Доказательство: config/store unit + browser smoke, + Codex; backend suite на Linux CI при изменении store schema. +- **AC3 — permissions.** Non-admin не получает активного control, не вызывает + ZHA/MQTT API и не видит слой при сохранённом `true`. Доказательство: unit + permission matrix + browser smoke, Codex. +- **AC4 — contextual only.** При включённой функции и пригодном snapshot без + hover нет линий, постоянных badges или легенды; hover одного mapped Zigbee + marker рисует только incident links этого node. Доказательство: resolver unit + + browser smoke + golden light/dark, Codex. +- **AC5 — no inferred route.** Реализация не строит путь до coordinator и не + добавляет несуществующие edges из route destinations. Доказательство: + provider fixtures + mutation witness, Codex. +- **AC6 — same-space mapping.** Рисуются только links с двумя exact drawable + endpoints текущего space; сосед получает transient neutral highlight. + Доказательство: mapping unit + browser smoke + golden, Codex. +- **AC7 — cross-space truth.** Remote endpoint не создаёт линию; рядом с + hovered marker временно показан неинтерактивный count только drawable remote + endpoints, без направления «выше/ниже». Доказательство: classification unit + + browser smoke + golden, Codex. +- **AC8 — ambiguous/hidden lifecycle.** Unmatched, ambiguous, hidden, removed и + HA-disabled endpoints не рисуются и не входят в cross-space count; rebind и + перемещение пересчитывают mapping без scan. Доказательство: unit + browser + smoke, Codex. +- **AC9 — modality and modes.** Touch/pen, keyboard focus, editors, kiosk и + `houseplan-space-card` не показывают topology и не получают нового действия; + существующие tap/long-press/click работают без изменений. Доказательство: + browser mouse/touch/mode matrix + existing device-action smokes, Codex. +- **AC10 — cleanup.** Pointerleave, mouse→touch/pen, mode/space change, + disconnect/remount и setting off очищают линии, highlight и count в том же + lifecycle turn. Доказательство: state-machine unit + browser smoke, Codex. +- **AC11 — pointer ownership.** Линии, highlight и count pointer-transparent, + не меняют device hit area, hover tooltip, click action, pan/zoom или fit. + Доказательство: DOM/computed-style assertions + browser gestures smoke, Codex. +- **AC12 — LQI visual.** Hover→neighbor observation использует каноническую + continuous LQI colour; unknown — нейтральный пунктир; reverse observation не + усредняется. Stroke остаётся читаемым в screen space на min/default/max zoom. + Доказательство: pure resolver unit + golden zoom/theme matrix, Codex. +- **AC13 — ZHA contract.** Явное действие читает admin-only `zha/devices`, + нормализует fixtures и не вызывает `zha/topology/update`; UI не заявляет + неизвестную scan freshness. Доказательство: adapter unit/contract fixture + + request spy, Codex. +- **AC14 — Z2M contract.** Только подтверждённая instance публикует raw + `routes:false` request после явного предупреждения; transaction, 150 s timeout, + retained/foreign/late response и cleanup соблюдены. Доказательство: adapter + unit with fake clock/MQTT + backend/HA contract smoke on Linux CI, Codex/CI. +- **AC15 — no implicit work.** HA state ticks, hover, space switch, rebind и + второй экземпляр карточки не запускают provider fetch/scan; concurrent explicit + requests дедуплицируются. Доказательство: call-count unit + browser smoke + + mutation witnesses, Codex. +- **AC16 — failures.** Unsupported API, permission denial, invalid payload, + partial provider failure и timeout дают локализованный status и не ломают plan + либо snapshot другого provider. Доказательство: unit/error fixtures + browser + smoke, Codex. +- **AC17 — privacy.** IEEE/raw topology не попадают в saved config, exports, + backup, diagnostics, support report, localStorage или console. Доказательство: + serialization/privacy tests + reviewer code audit, Codex/Claude. +- **AC18 — bounds.** Payload limits, edge cap и incident-only render сохраняют + установленный performance budget на fixtures 20/100/500 nodes и dense + malformed graph. Доказательство: benchmark + performance smoke, Codex/CI. +- **AC19 — lazy boundary.** Выключенный initial View не включает topology runtime + в initial graph и сохраняет действующий bundle ceiling; включённый путь + загружает отдельный chunk один раз. Доказательство: chunk/import unit + bundle + budget + browser network spy, Codex. +- **AC20 — i18n.** Все новые видимые состояния имеют parity en/ru/de/fr, + корректные plurals и не показывают raw provider text. Доказательство: i18n + parity/dead-key tests + golden, Codex. + +## 14. План автотестов + +1. Добавить pure-unit suites для settings normalization, normalized graph, + directional pair merge, incident-edge selection, exact registry/marker + mapping, cross-space classification, payload bounds и cache/in-flight dedupe. +2. Добавить обезличенные ZHA и Z2M fixtures: one-way/two-way/unknown LQI, + sleeping end device, coordinator, multiple instances, duplicates/self links, + partial router failure, wrong base topic и malformed/dense payload. +3. Adapter tests с fake clock/transport доказывают ZHA read-only path и полную + Z2M transaction/timeout/retained/late/cleanup матрицу. Для защит implicit scan, + matching и cleanup зарегистрировать mutation-gate entries. +4. Новый browser smoke `smoke_zigbee_topology_hover.mjs`: off/no requests; + admin enable/persistence; explicit load; mouse hover incident-only; leave и + lifecycle cleanup; same/cross-space; touch/pen/mode/kiosk/static absence; + click/pan/zoom regression; errors и second-card dedupe. +5. Golden-сценарии light/dark: local strong/weak/unknown links, cross-space count, + min/default/max zoom и partial-data status в общих настройках. Baselines + принимаются только через штатный Linux reviewed workflow. +6. Performance fixture 20/100/500 nodes измеряет normalize/map, first hover, + repeated hover и dense-invalid rejection; обычный HA tick сохраняет нулевую + topology работу. Бюджеты фиксируются до S7 на измеренном dev-стенде. +7. Если реализация добавляет House Plan backend surface, выполнить полный HA + harness на Linux CI; Windows native результат не считать backend-доказательством. +8. Перед S7 обязательны `npm run typecheck`, `npm test`, `npm run build`, + `npm run bundle:sync`, `npm run bundle:budget`, `node scripts/check-docs.mjs`, + `node scripts/smoke-select.mjs --base origin/dev --head HEAD`, выбранные smoke, + `node scripts/no-new-any.mjs --base origin/dev --head HEAD` и относящиеся + mutation runs. Golden/performance полным набором — перед бетой, targeted + witnesses — до код-ревью. + +## 15. Риски + +- ZHA snapshot может быть старым, а completion-aware refresh отсутствует. +- Z2M scan дорогой и зависит от корректного base topic/MQTT permissions. +- Один HA device может иметь несколько маркеров, а custom discovery identifiers + могут разрушить exact mapping. +- Пользователь может принять neighbor snapshot за реальный маршрут трафика. +- Dense/ошибочный payload способен перегрузить normalization или DOM без ранних + limits и incident-only resolver. +- Hover-only UX недоступен на touch; это осознанное ограничение диагностической + функции, а не скрытая попытка переопределить device gestures. + +## 16. Откат + +Функция fail-closed и выключена по умолчанию. Операционный откат — отключить +настройку: runtime перестаёт загружаться/рисоваться, provider requests не +выполняются, текущий план и LQI остаются прежними. Каждый provider adapter можно +отключить независимо без изменения данных плана. Snapshot memory-only, поэтому +cleanup пользовательских данных и миграция назад не требуются; сохранённые base +topics безопасно игнорируются старой версией. + +## 17. Release-артефакты + +- запись со ссылкой на #54 в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том + же user-visible implementation commit; +- актуализация `docs/UX-MODES.md`, `docs/SCOPE.md`/J7 и пользовательского + руководства: default-off, mouse-only, incident-only, manual provider refresh; +- обновление описания общих настроек и privacy/support boundary; +- reviewed golden screenshots для topology hover и настроек; +- release note прямо сообщает ограничения: admin, mouse hover, snapshot, без + ZHA forced scan и без touch interaction. + +## 18. Предположения автора — можно менять на ревью без решения владельца + +- Внутреннее поле настроек группируется в `settings.zigbee_topology`; точное имя + не является продуктовым контрактом. +- Provider-neutral runtime и adapters живут в отдельном lazy chunk; House Plan + backend не добавляется, пока штатных HA WebSocket/MQTT surfaces достаточно. +- Default Z2M base topic — `zigbee2mqtt`; несколько instances представлены + списком exact base topics. +- Stale threshold — пять минут; он меняет только status, не запускает refresh. +- Z2M hard timeout — 150 секунд, `routes:false`. +- Line visual использует LQI направления hovered→neighbor; reverse observation + хранится, но не требует второго параллельного stroke. +- Cross-space detail ограничен неинтерактивным count; список и навигация могут + стать отдельной задачей только после полевого запроса. + +## 19. Источники Stage 0 + +- Home Assistant Core ZHA WebSocket API: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/zha/websocket_api.py +- Home Assistant Frontend ZHA data contract: + https://github.com/home-assistant/frontend/blob/dev/src/data/zha.ts +- Home Assistant Frontend graph normalization: + https://github.com/home-assistant/frontend/blob/dev/src/panels/config/integrations/integration-panels/zha/zha-network-data.ts +- zigpy topology scanner: + https://github.com/zigpy/zigpy/blob/dev/zigpy/topology.py +- Home Assistant MQTT WebSocket subscription: + https://github.com/home-assistant/core/blob/dev/homeassistant/components/mqtt/__init__.py +- Home Assistant `mqtt.publish` action: + https://www.home-assistant.io/actions/mqtt.publish/ +- Zigbee2MQTT networkmap API: + https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqttbridgerequestnetworkmap +- Zigbee2MQTT implementation/model: + https://github.com/Koenkk/zigbee2mqtt/blob/master/lib/extension/networkMap.ts + and https://github.com/Koenkk/zigbee2mqtt/blob/master/lib/types/api.ts