diff --git a/docs/specs/464-zigbee-topology-layer-order.md b/docs/specs/464-zigbee-topology-layer-order.md new file mode 100644 index 00000000..fbefda4d --- /dev/null +++ b/docs/specs/464-zigbee-topology-layer-order.md @@ -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 + +`` монтируется дочерним элементом `.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 реализуется вторым совпадающим `` перед 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. diff --git a/docs/specs/README.md b/docs/specs/README.md index 82eb820b..9c15b091 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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