32 KiB
#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-топологии внутри содержимого плана, сверху вниз:
- полные маркеры устройств — концов реально показанных локальных связей: исходный hovered marker и все drawable local neighbor markers;
- все элементы активной топологии: lines, route arrows, halos, remote count и parent/space/unplaced bubbles;
- все посторонние маркеры, названия комнат и остальное содержимое плана.
Системные панели, диалоги, глобальные tooltips и служебные overlays карточки остаются выше топологии. Весь topology overlay остаётся pointer-transparent.
Unknown-LQI link получает внешнюю окантовку 1 CSS px цветом #2e2e2e; основной
серый пунктир, его 2 px ширина, dash rhythm, геометрия и прозрачные промежутки
сохраняются.
5. Скоуп
- Поместить lazy topology overlay в тот же локальный camera/stacking context, что device markers и room labels.
- Задать однозначные локальные уровни для обычного содержимого, topology и активных локальных endpoints.
- Поднимать только точное множество полных marker-элементов, являющихся концами реально нарисованных локальных links.
- Полностью очищать transient endpoint ownership вместе с hover overlay во всех действующих lifecycle exits и при замене marker DOM.
- Не применять отдельную live-camera projection к вложенному overlay: он
должен двигаться один раз вместе с
.devlayer. - Добавить casing только серому пунктиру local link с неизвестным LQI.
- Расширить целевой browser smoke фактическими пересечениями, raster probes, lifecycle и live-camera проверками; добавить дорогие защитные мутанты.
- Актуализировать прежние ТЗ, архитектуру, 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 равен:
{ 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:
- casing первым:
#2e2e2e, ширина 4 CSS px; - существующий 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, наблюдает только.devlayerchild-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. План автотестов и отрицательные доказательства
- Расширить
demo/smoke_zigbee_topology_hover.mjsfixture как минимум тремя local markers: source, drawable neighbor и unrelated marker, плюс room label; принудительно расположить пересечения в известных screen coordinates. - Проверить document/stacking context: overlay является ребёнком
.devlayer, не имеет отдельного live-camera attribute, обычные объекты ниже, exact endpoint markers выше. - Снять stage screenshot в самом smoke и прочитать pixels через
createImageBitmap/OffscreenCanvas: линия видна поверх unrelated marker и room label, но не поверх центров source/neighbor. Отдельно навести настоящий Playwright-курсор черезpage.mouse.moveна source и проверить, что его вычисленныйz-indexостаётся выше overlay при активном CSS:hover; синтетическийdispatchEvent(pointerover)эту ветку каскада не покрывает. - Для unknown-LQI link проверить два совпадающих strokes и растровую последовательность core → dark casing → background/gap при DPR 1; повторить screen-space geometry на wide/tall и min/default/max zoom.
- Проверить known-LQI local link и gray solid parent route отрицательно: casing отсутствует.
- Расширить существующий lifecycle smoke: pointerleave, touch, mode, space, setting off, marker DOM replacement и overlay disconnect не оставляют endpoint attributes; click сквозь overlay достигает прежнего target.
- В transient live-camera frame вручную применить одну non-identity projection штатным viewport helper и доказать общую матрицу parent/markers/overlay; после commit transform отсутствует, geometry совпадает.
- Добавить в
scripts/mutation-gate.mjsдорогие mutants для AC1–AC3, чтобы отзыв слоя, endpoint promotion/cleanup и casing нельзя было принять одним зелёным DOM-тестом, не чувствительным к дефекту. - Перед
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фиксирует один.devlayerstacking 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/**требует полного актуального LinuxDocs screenshotsartifact и reviewed acceptance всех кадров по общему процессу; кадры не принимаются с Windows; - performance/security отдельных отчётов не требуется; обязательны
benchmark:zigbee-topology,bundle:budget, mutation и штатные beta gates; - generated bundle trees обновляются только через
npm run bundle:sync.
20. Принятые технические предположения — можно менять на ревью
- Рекомендуемые local levels 7/8 не являются публичными; важен строгий порядок
§7.1 и неизменный внешний
.devlayerlevel. - Namespaced endpoint attribute живёт на root
.dev, потому что так единым уровнем поднимаются shell, badges, pulse и hit surface без дублирования renderer. - Overlay сам синхронизирует attributes, чтобы mouse hover не становился reactive state full card и не возвращал регрессию производительности #451.
- Bounded
MutationObserver(childList)допустим только если Lit способен заменить marker DOM без изменения child properties; реализация может выбрать эквивалентный lifecycle hook с теми же AC. - Casing реализуется вторым совпадающим
<line>перед core, а не SVG filter: это сохраняет deterministic screen-space geometry и позволяет отдельно проверить прозрачный gap. - Parent-route line к bubble остаётся без casing: решение владельца относится к серым пунктирным links с неизвестным LQI, а этот route сплошной и имеет другую семантику.
- Raster probes в целевом smoke являются достаточным визуальным witness без расширения общей golden matrix, поскольку topology существует только во время программно созданного admin mouse hover.