docs: specify Zigbee topology layer order

Issue: #464
User-Visible: no
This commit is contained in:
Matysh
2026-09-06 00:08:16 +03:00
parent ce07db748e
commit aae9efb9ff
2 changed files with 457 additions and 1 deletions
@@ -0,0 +1,455 @@
# #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.
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.
+2 -1
View File
@@ -1,6 +1,6 @@
# Спецификации задач
Актуально на 2026-09-04.
Актуально на 2026-09-06.
GitHub Issues и GitHub Projects (v2) остаются единственным каноническим backlog проекта. Этот каталог содержит развёрнутые ТЗ: каждое ТЗ ссылается на issue, а issue — на соответствующий файл. Статус, приоритет и факт завершения меняются только в GitHub.
@@ -183,6 +183,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#457](https://github.com/Matysh/houseplan-card/issues/457) Направление Zigbee-связей к координатору | [457-zigbee-route-arrows.md](457-zigbee-route-arrows.md) |
| [#460](https://github.com/Matysh/houseplan-card/issues/460) Детерминированное завершение кадра живого редактора | [460-live-editor-settlement.md](460-live-editor-settlement.md) |
| [#461](https://github.com/Matysh/houseplan-card/issues/461) Быстрый commit промежуточной точки цепочки стен | [461-wall-draw-click-performance.md](461-wall-draw-click-performance.md) |
| [#464](https://github.com/Matysh/houseplan-card/issues/464) Верхний контекстный слой Zigbee-топологии | [464-zigbee-topology-layer-order.md](464-zigbee-topology-layer-order.md) |
## P3