Files
houseplan-card/docs/specs/464-zigbee-topology-layer-order.md
T
2026-09-06 01:23:51 +03:00

459 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #464 — Верхний контекстный слой Zigbee-топологии
- **Issue:** https://github.com/Matysh/houseplan-card/issues/464
- **Тип / приоритет:** bug + polish / P2
- **Трек:** полный; задача намеренно меняет публичный порядок слоёв, ранее
закреплённый в #54 и #457, и пересекает full-card compositor, lazy overlay и
live-camera projection
- **Оценка:** пользовательская ценность 6/10; ценность для разработки 5/10;
сложность 5/10; риск 5/10
- **Связано:** #54, #457, #459; `docs/SCOPE.md`,
`docs/TOUCH-SUPPORT.md`, `docs/ARCHITECTURE.md`
## 1. Сценарий
Персона — Home admin из `docs/SCOPE.md`. Поверхность — полная карточка House
Plan в режиме View на desktop с реальной мышью. Момент — администратор уже
включил контекстную Zigbee-диагностику, загрузил snapshot ZHA или Zigbee2MQTT и
наводит указатель на устройство, чтобы понять его прямые связи и следующий шаг
к координатору.
На насыщенном плане линия, стрелка или поясняющая плашка проходит через название
комнаты либо через посторонний маркер. Сейчас содержимое плана закрывает
диагностику именно там, где её нужно прочитать.
## 2. Что человек увидит до и после
**До:** названия комнат и любые маркеры устройств могут перекрыть линии,
стрелки и подписи активной Zigbee-топологии; серый пунктир неизвестного LQI
теряется на части заливок.
**После:** активная топология читается поверх содержимого плана, но визуально
уходит под полные маркеры именно тех устройств, которые соединяет; серый
пунктир получает тонкую тёмную окантовку с сохранёнными просветами.
## 3. Подтверждённая причина и изменяемый контракт
Текущая композиция соответствует старому ТЗ, а не случайной CSS-ошибке:
- `docs/specs/054-zigbee-topology-overlay.md` §6 и AC6 закрепляют overlay над
архитектурой/decor, но под всеми device markers;
- `docs/specs/457-zigbee-route-arrows.md` §8 оставляет arrow/bubble в том же
слое и AC5 обещает не менять unknown-пунктир;
- `docs/ARCHITECTURE.md` повторяет этот порядок;
- `src/houseplan-card.ts` монтирует lazy overlay перед `.devlayer`;
- host overlay имеет `z-index: 5`, а `.devlayer` — отдельный stacking context с
`z-index: 6`;
- unknown-LQI link сейчас является одним серым dashed stroke без casing.
Поэтому простое увеличение `z-index` у существующего sibling overlay решает
только половину задачи: весь `.devlayer` останется единым context ниже или выше
него, а дочерний endpoint marker не сможет выйти поверх соседнего stacking
context. Снятие context с `.devlayer` также неприемлемо: во время live pan/zoom
родитель получает CSS transform и снова образует context, отчего settled и
intermediate кадры разойдутся.
#464 явно заменяет только прежний контракт порядка слоёв и оформления
unknown-LQI line. Семантика snapshot, mapping, route tree и hover не меняется.
## 4. Решение владельца
Визуальный порядок активной Zigbee-топологии внутри содержимого плана, сверху
вниз:
1. полные маркеры устройств — концов реально показанных локальных связей:
исходный hovered marker и все drawable local neighbor markers;
2. все элементы активной топологии: lines, route arrows, halos, remote count и
parent/space/unplaced bubbles;
3. все посторонние маркеры, названия комнат и остальное содержимое плана.
Системные панели, диалоги, глобальные tooltips и служебные overlays карточки
остаются выше топологии. Весь topology overlay остаётся pointer-transparent.
Unknown-LQI link получает внешнюю окантовку 1 CSS px цветом `#2e2e2e`; основной
серый пунктир, его 2 px ширина, dash rhythm, геометрия и прозрачные промежутки
сохраняются.
## 5. Скоуп
1. Поместить lazy topology overlay в тот же локальный camera/stacking context,
что device markers и room labels.
2. Задать однозначные локальные уровни для обычного содержимого, topology и
активных локальных endpoints.
3. Поднимать только точное множество полных marker-элементов, являющихся
концами реально нарисованных локальных links.
4. Полностью очищать transient endpoint ownership вместе с hover overlay во
всех действующих lifecycle exits и при замене marker DOM.
5. Не применять отдельную live-camera projection к вложенному overlay: он
должен двигаться один раз вместе с `.devlayer`.
6. Добавить casing только серому пунктиру local link с неизвестным LQI.
7. Расширить целевой browser smoke фактическими пересечениями, raster probes,
lifecycle и live-camera проверками; добавить дорогие защитные мутанты.
8. Актуализировать прежние ТЗ, архитектуру, User Guide EN/RU, testing contract,
STATUS и оба changelog.
## 6. Не входит
- получение, срок жизни, ручное обновление и cache topology snapshot;
- ZHA/Zigbee2MQTT adapters, matching IEEE → marker и privacy contract;
- состав incident links, parent tree, направление стрелок и remote count;
- цветовая шкала известного LQI, halo, тексты или размещение bubbles, кроме
недопущения перекрытия endpoint marker;
- новые настройки, config/storage поля, миграция либо backend API;
- topology в kiosk, редакторах, static card, touch/pen или keyboard focus;
- изменение маркеров, room labels, live vacuum, opening locks, fit/bounds,
pan/pinch или device actions;
- общий рефакторинг слоёв карточки за пределами topology;
- исправление возможных пересечений нескольких линий друг с другом: они
являются одним диагностическим слоем и рисуются по существующему порядку.
Если для решения потребуется изменить mapping, route selection, bubble copy,
config или публичный жест, задача возвращается в `S3-spec` как расширение
скоупа.
## 7. Контракт композиции
### 7.1 Один локальный stacking context
`<hp-zigbee-topology-overlay>` монтируется дочерним элементом `.devlayer`, а не
отдельным sibling между plan SVG и `.devlayer`. Внешний `.devlayer` сохраняет
действующий stage-level `z-index: 6`, поэтому весь пакет остаётся:
- выше plan/architectural/isometric слоёв;
- ниже editor chrome, recovery/loading overlays, zoom/home UI, tooltip и
диалогов;
- в одном camera context с HTML markers и room labels.
Внутри `.devlayer` нормативный относительный порядок такой:
| Слой | Нормативный локальный уровень | Содержимое |
|---|---:|---|
| Обычное plan HTML | ниже topology | room labels, opening locks, обычные `.dev`, vacuum trail/puck и прочие посторонние markers |
| Active topology | выше любого обычного plan HTML | line casing/core, arrows, halo, remote count, parent bubbles |
| Active endpoints | выше topology | полный `.dev` исходного marker и каждого drawable local neighbor |
Точные числовые `z-index` являются внутренним решением. Они должны образовывать
строгий порядок и не пересекаться с stage chrome. Один рекомендуемый вариант:
обычные дочерние элементы сохраняют текущие 1…6, topology получает 7, endpoints
— 8.
Topology host и каждый его видимый потомок остаются `pointer-events:none`.
Повышение слоя не меняет hit testing: клик, hover, right click, long press и
pan продолжают получать нижележащие существующие targets.
### 7.2 Множество активных endpoints
Transient set равен:
```text
{ hovered source marker }
∪ { neighborMarkerId каждого local line, который фактически отрисован }
```
Правила:
- source поднимается только когда для hover существует хотя бы один видимый
topology primitive: local line/arrow/halo, remote count или parent bubble;
- local neighbor поднимается только если его DOM marker найден и соответствующая
линия действительно вошла в rendered set;
- remote-space, unplaced, ambiguous, hidden, removed и HA-disabled target не
создаёт и не поднимает вымышленный marker;
- один marker, участвующий в нескольких links, поднимается один раз;
- посторонний hovered/focused marker не становится endpoint только из-за своего
обычного CSS hover/focus уровня;
- transient ownership маркируется namespaced DOM-атрибутом, не сохраняется в
config/layout/localStorage и не попадает в экспорт;
- полный marker означает shell, core, icon/value, LQI/value badges, status dots,
pulse и остальные уже принадлежащие ему визуальные части.
Overlay остаётся владельцем mouse-hover state и endpoint set. Родительская full
card не получает новый reactive state и не делает тяжёлый render на
pointerover/pointerout.
### 7.3 Cleanup и замена DOM
Endpoint attribute снимается в тот же lifecycle turn, что и визуальная
топология, при каждом из событий:
- pointerleave исходного marker;
- реальный touch/pen input или потеря mouse-hover gate;
- смена space или режима;
- выключение topology setting;
- смена devices/registry/runtime, после которой endpoint больше не drawable;
- disconnected/remount overlay;
- удаление либо замена marker DOM родительским Lit render.
После любого cleanup в `.devlayer` не остаётся marker с endpoint-атрибутом.
Если активный marker DOM заменён эквивалентным элементом при допустимом render,
ownership переносится на новый элемент без закрепления на старом. Для этого
допустим ограниченный `MutationObserver` только за `childList`; он не наблюдает
собственные attribute changes и отключается в `disconnectedCallback`.
### 7.4 Live camera
Вложенный overlay не имеет собственного `data-hp-live-layer="camera"`. Во время
быстрого pan/zoom `live-viewport.ts` трансформирует `.devlayer` целиком, поэтому
markers, labels и topology получают одну и ту же projection ровно один раз.
После terminal Lit frame `viewKey` остаётся сигналом пересчитать screen-space
marker centres и arrow geometry. Контракт проверяется как во временном кадре с
неединичной parent transform, так и после settled frame: линия не отстаёт, не
удваивает сдвиг и не меняет endpoint ownership.
## 8. Контракт unknown-LQI casing
Только локальная link line, у которой выбранное directional observation не
содержит LQI, рисуется двумя совпадающими dashed strokes:
1. casing первым: `#2e2e2e`, ширина 4 CSS px;
2. существующий neutral-gray core вторым: ширина 2 CSS px.
Оба stroke имеют одну геометрию, `stroke-dasharray: 5 5`, одинаковые dash
offset/linecap, opacity contract и `vector-effect: non-scaling-stroke`. Разница
общей ширины 2 px даёт по 1 CSS px окантовки с каждой стороны. Casing не должен
заполнять gap целиком: между соседними видимыми dash остаётся хотя бы один
прозрачный raster column при DPR 1 в целевом smoke.
Известный LQI остаётся одним цветным solid stroke. Solid parent route к bubble
не получает casing, даже если он серый. Route arrow сохраняет текущий fill и не
получает отдельную обводку в этой задаче.
В `forced-colors` действующий системный override сильнее точного `#2e2e2e`:
line/arrow продолжают использовать `Highlight`, а bubble —
`CanvasText`/`Canvas`. Это accessibility contract, а не визуальная регрессия.
## 9. Режимы, touch и доступность
- Функция по-прежнему существует только в full-card View, для HA admin, после
настоящего mouse hover и при включённой setting.
- Touch/pen немедленно очищают topology и endpoint promotion. Tap/long press не
получают topology path и не меняются.
- Kiosk, Plan/Devices/Background editors, `houseplan-space-card` и non-admin не
монтируют overlay и не оставляют transient endpoint attributes.
- Keyboard focus не создаёт topology, но обычный focus marker продолжает
работать по существующему CSS.
- Overlay не получает role, tab stop или accessible text: это временное
визуальное продолжение уже существующей admin-only диагностики.
- `prefers-reduced-motion` не затрагивается: новой анимации нет.
Таким образом, View/touch safety floor из `docs/TOUCH-SUPPORT.md` сохраняется;
нового touch UX и сознательной деградации нет.
## 10. Модель данных, миграция и совместимость
Config, layout, topology runtime model, provider payload и WebSocket/MQTT API
не меняются. Endpoint promotion — transient DOM state одного экземпляра full
card.
Миграции, compatibility-полей и downgrade converter нет. Старый frontend после
downgrade читает тот же config и просто возвращает прежний порядок слоёв.
## 11. i18n
Новых или изменённых пользовательских строк нет. Словари EN/RU/DE/FR и
topology namespace не меняются.
## 12. Производительность и lazy boundary
- Disabled topology по-прежнему не загружает lazy overlay chunk.
- Изменение не запускает provider fetch, topology normalization, route BFS,
mapping или full-card render на hover.
- Endpoint sync ограничен marker DOM текущего space и выполняется только при
смене hover/rendered endpoints либо замене marker DOM; HA state tick без
изменения этих границ не должен создавать новый observer или цикл update.
- `MutationObserver`, если используется, один на mounted overlay, наблюдает
только `.devlayer` child-list и обязательно отключается при disconnect.
- Два SVG stroke вместо одного создаются только для видимых unknown-LQI local
links активного hover. Постоянного DOM или работы при отсутствии hover нет.
- Initial View graph, topology lazy chunk и bundle budget не должны превысить
действующие ceilings. Новый full performance profile не требуется; целевой
topology smoke и `bundle:budget` обязательны.
## 13. Security и privacy
Новых API, permission или persistence surfaces нет. DOM-атрибут содержит только
уже существующую роль endpoint без IEEE, raw payload, entity/device/space id в
значении. Существующие marker `data-id` не расширяются и не логируются.
Pointer transparency обязательна: поднятый overlay не может перехватить
устройство, комнату либо жест сцены. Пользовательский текст bubbles продолжает
выводиться безопасным Lit text binding.
## 14. Критерии приёмки
### AC1 — однозначный визуальный порядок (`smoke` + raster + ревью кода)
При активном hover полные source и все drawable local neighbor markers
находятся выше lines/arrows/halos/bubbles/remote count. Вся topology находится
выше намеренно пересекающих её unrelated `.dev`, `.roomlabel`, vacuum
trail/puck, opening lock и plan/iso/decor. Stage chrome, tooltip и dialogs
остаются выше topology.
Доказательство: расширенный `smoke_zigbee_topology_hover.mjs` строит
детерминированные пересечения, проверяет computed stacking levels и raster
pixels в точках topology-over-unrelated и endpoint-over-topology. Мутанты
«вернуть overlay sibling под `.devlayer`» и «не поднимать endpoints» краснеют.
### AC2 — ownership, lifecycle и camera invariants (`smoke` + mutation)
Endpoint set точно равен source + endpoints реально нарисованных local links;
remote/unplaced targets не поднимают marker. Overlay и все его primitives
pointer-transparent. Leave, touch/pen, hover-gate loss, mode/space/setting
change, devices/registry/runtime invalidation, DOM replacement и disconnect
очищают либо правильно переносят namespaced attributes. Во время live pan/zoom
topology получает ровно parent projection, а settled frame сохраняет alignment.
Действующие disabled/lazy, click/tap/room-fit/pan contracts не меняются.
Доказательство: browser lifecycle/click/gesture/live-transform matrix.
Защитный мутант, отключающий cleanup или возвращающий отдельный camera attribute
overlay, обязан краснеть.
### AC3 — casing неизвестного LQI (`smoke` + raster + mutation)
Каждый local unknown-LQI link содержит сначала casing `#2e2e2e` 4 px, затем
существующий gray core 2 px; оба имеют одинаковые `5 5`, geometry, offset,
linecap и non-scaling stroke. Raster probe подтверждает 1 px тёмную кромку и
хотя бы один прозрачный column в gap при DPR 1, а min/default/max zoom сохраняют
screen-space толщину. Known-LQI links и solid parent route не получают casing;
forced-colors сохраняет системную палитру.
Доказательство: DOM/computed-style assertions, deterministic raster probes и
мутанты удаления casing, неверного цвета/ширины либо solid gap.
## 15. План автотестов и отрицательные доказательства
1. Расширить `demo/smoke_zigbee_topology_hover.mjs` fixture как минимум тремя
local markers: source, drawable neighbor и unrelated marker, плюс room label;
принудительно расположить пересечения в известных screen coordinates.
2. Проверить document/stacking context: overlay является ребёнком `.devlayer`,
не имеет отдельного live-camera attribute, обычные объекты ниже, exact
endpoint markers выше.
3. Снять stage screenshot в самом smoke и прочитать pixels через
`createImageBitmap`/`OffscreenCanvas`: линия видна поверх unrelated marker и
room label, но не поверх центров source/neighbor. Отдельно навести настоящий
Playwright-курсор через `page.mouse.move` на source и проверить, что его
вычисленный `z-index` остаётся выше overlay при активном CSS `:hover`;
синтетический `dispatchEvent(pointerover)` эту ветку каскада не покрывает.
4. Для unknown-LQI link проверить два совпадающих strokes и растровую
последовательность core → dark casing → background/gap при DPR 1; повторить
screen-space geometry на wide/tall и min/default/max zoom.
5. Проверить known-LQI local link и gray solid parent route отрицательно: casing
отсутствует.
6. Расширить существующий lifecycle smoke: pointerleave, touch, mode, space,
setting off, marker DOM replacement и overlay disconnect не оставляют
endpoint attributes; click сквозь overlay достигает прежнего target.
7. В transient live-camera frame вручную применить одну non-identity projection
штатным viewport helper и доказать общую матрицу parent/markers/overlay;
после commit transform отсутствует, geometry совпадает.
8. Добавить в `scripts/mutation-gate.mjs` дорогие mutants для AC1–AC3, чтобы
отзыв слоя, endpoint promotion/cleanup и casing нельзя было принять одним
зелёным DOM-тестом, не чувствительным к дефекту.
9. Перед `S7-code-review`: `npx tsc --noEmit`, `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`, выбранные topology/device-action/viewport smokes,
`npm run golden:verify`, `npm run benchmark:zigbee-topology` и
`node scripts/no-new-any.mjs --base origin/dev --head HEAD`.
Существующая golden matrix не активирует admin mouse-hover topology, поэтому
новый постоянный golden baseline не вводится. Визуальный контракт закрывает
детерминированный raster smoke, который падает по пикселям, а не только по DOM.
`golden:verify` подтверждает отсутствие регрессий остальных сцен.
## 16. Затронутые файлы и модули
Ожидаемый продуктовый минимум:
- `src/houseplan-card.ts` — topology host внутри `.devlayer`;
- `src/zigbee-topology-overlay-bridge.ts` — исключение двойной camera
projection;
- `src/hp-zigbee-topology-overlay.ts` — локальный уровень, endpoint ownership,
cleanup и unknown-LQI casing;
- `src/styles/devices.styles.ts` и при необходимости
`src/styles/plan.styles.ts` — namespaced endpoint layer contract.
Проверки и документы:
- `demo/smoke_zigbee_topology_hover.mjs`;
- `scripts/mutation-gate.mjs` и smoke-selection registry при необходимости;
- `docs/specs/054-zigbee-topology-overlay.md` и
`docs/specs/457-zigbee-route-arrows.md` — явная отметка о заменённом #464
порядке слоёв/unknown casing;
- `docs/ARCHITECTURE.md`, `docs/USER-GUIDE.md`,
`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`, `docs/STATUS.md`;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
- manifest-driven `dist/**` и integration frontend после `bundle:sync`;
- docs screenshot index/frames после штатного Linux capture, потому что
меняется `src/**`.
Backend Python, config schema и i18n не меняются.
## 17. Риски и меры
| Риск | Мера |
|---|---|
| Простое повышение sibling `z-index` накрывает endpoints или остаётся под всем `.devlayer` | Один stacking context внутри `.devlayer`, отдельный exact endpoint level |
| Двойной сдвиг при быстром pan/zoom | У вложенного overlay нет собственного camera projection; live-transform smoke |
| Transient attribute залипает после смены режима/DOM | Единый owner set, полный cleanup matrix, bounded child-list observer |
| Линия перекрывает badge/pulse связанного marker | Поднимается полный root `.dev`, raster probe на endpoint centre и shell edge |
| Topology закрывает системную кнопку/tooltip | Внешний `.devlayer` level не меняется, chrome/dialog levels проверяются |
| Поднятый overlay крадёт click/pan | `pointer-events:none` на host/primitives + сквозной click/gesture smoke |
| Casing превращает пунктир в сплошную линию | Одинаковый `5 5`, screen-space strokes и pixel probe прозрачного gap |
| Forced colors теряет системный контраст | Существующий `!important Highlight` остаётся authority |
| Hover начинает перерендеривать всю карточку | Ownership остаётся внутри lazy child; parent reactive state не добавляется |
| Observer создаёт цикл или leak | Только `childList`, один observer, disconnect cleanup и call-count assertion |
## 18. Rollback
Кодовый откат одного продуктового коммита возвращает topology host отдельным
sibling под `.devlayer`, удаляет endpoint promotion и второй unknown-LQI stroke.
Config, layout и provider cache не мигрируют, поэтому data rollback и ручное
вмешательство не нужны.
Пользовательский немедленный workaround до отката — выключить существующую
опцию Zigbee links; весь lazy overlay исчезает как раньше.
## 19. Release-артефакты
Поскольку исправление пользовательски видимо:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` получают короткий пункт со
ссылкой на #464;
- User Guide EN/RU объясняет приоритет активной topology над посторонним
содержимым и сохранение endpoints сверху;
- `docs/ARCHITECTURE.md` фиксирует один `.devlayer` stacking context и
transient endpoint ownership;
- исходные ТЗ #54/#457 получают ссылку, что #464 заменил только прежний layer и
unknown-dash visual contract;
- `docs/TESTING.md` связывает AC с topology raster/lifecycle smoke;
- постоянный product golden не добавляется по причине из §15;
`golden:verify` остаётся обязательным;
- изменение `src/**` требует полного актуального Linux `Docs screenshots`
artifact и reviewed acceptance всех кадров по общему процессу; кадры не
принимаются с Windows;
- performance/security отдельных отчётов не требуется; обязательны
`benchmark:zigbee-topology`, `bundle:budget`, mutation и штатные beta gates;
- generated bundle trees обновляются только через `npm run bundle:sync`.
## 20. Принятые технические предположения — можно менять на ревью
1. Рекомендуемые local levels 7/8 не являются публичными; важен строгий порядок
§7.1 и неизменный внешний `.devlayer` level.
2. Namespaced endpoint attribute живёт на root `.dev`, потому что так единым
уровнем поднимаются shell, badges, pulse и hit surface без дублирования
renderer.
3. Overlay сам синхронизирует attributes, чтобы mouse hover не становился
reactive state full card и не возвращал регрессию производительности #451.
4. Bounded `MutationObserver(childList)` допустим только если Lit способен
заменить marker DOM без изменения child properties; реализация может выбрать
эквивалентный lifecycle hook с теми же AC.
5. Casing реализуется вторым совпадающим `<line>` перед core, а не SVG filter:
это сохраняет deterministic screen-space geometry и позволяет отдельно
проверить прозрачный gap.
6. Parent-route line к bubble остаётся без casing: решение владельца относится
к серым пунктирным links с неизвестным LQI, а этот route сплошной и имеет
другую семантику.
7. Raster probes в целевом smoke являются достаточным визуальным witness без
расширения общей golden matrix, поскольку topology существует только во
время программно созданного admin mouse hover.