# Issue #90 — управляемый бейдж со значением рядом с устройством - **Статус:** реализовано локально / ревизия 2 после ревью 2026-08-11; ожидает beta-проверки - **Issue:** https://github.com/Matysh/houseplan-card/issues/90 - **Область:** frontend, marker config, backend validation, import/export, документация и QA - **Приоритет:** P2 - **Тип:** feature / UX ## 1. Резюме В настройках каждого устройства появляется явная настройка отдельного бейджа со значением. Пользователь самостоятельно решает: 1. нужен ли бейдж; 2. какое конкретно значение Home Assistant он показывает; 3. с какой стороны маркера он расположен: справа, снизу, слева или сверху. Checkbox, источник и положение являются live-настройками: существующий блок предпросмотра обязан сразу показывать итоговый marker вместе с бейджем именно так, как он будет выглядеть на плане. Новая настройка заменяет неявный выбор температуры/влажности по иконке и `primary`-сущности для явно настроенных устройств. Старые нетронутые конфигурации продолжают выглядеть как раньше. Бейдж является независимой поверхностью и не заменяет существующий режим «Значение вместо иконки». ## 2. Проблема текущей реализации Сейчас компактное значение около маркера определяется скрытыми эвристиками: - температура появляется только у `mdi:thermometer`, `mdi:air-filter` либо при включённом `use_climate_temp`; - влажность появляется только тогда, когда именно `primary`-сущность признана датчиком влажности; - температура и влажность не имеют пользовательского выбора источника; - два показателя потенциально претендуют на одно и то же место справа; - LQI всегда располагается снизу и не участвует в раскладке с другими показателями; - изменение автоматической иконки или порядка сущностей может незаметно изменить отображение; - глобальное поле `show_temperature` фактически управляет и температурой, и влажностью, но не объясняет конкретный выбор источника; - режим «Значение вместо иконки» решает другую задачу и не позволяет оставить обычную иконку с выбранным показанием рядом. Результат непредсказуем: пользователь видит либо случайно подходящее значение, либо не видит ничего и не может объяснить или исправить результат в UI. ## 3. Пользовательская ценность ### 3.1. Оценка **Ценность: высокая, 8/10.** Это не декоративная настройка, а устранение необъяснимого поведения одного из основных элементов плана. Функция позволяет без отдельных карточек показывать рядом с устройством: - температуру, влажность, заряд батареи, мощность, расход, давление; - состояние бинарного датчика или реле; - положение шторы/клапана; - текущую температуру climate-устройства; - громкость media player; - средний LQI; - состояние явно управляемого источника. Особенно полезны мультисенсоры и сложные HA-устройства, у которых десятки сущностей и текущая эвристика почти неизбежно выбирает не то либо ничего. ### 3.2. Почему это лучше автоматической эвристики - результат объясним и воспроизводим; - изменение иконки не меняет значение; - один marker показывает ровно один выбранный внешний бейдж; - пользователь может осознанно распределить бейджи вокруг плотной группы; - preview становится достоверным инструментом настройки; - сохранённая ссылка переживает перестановку сущностей в реестре HA. ## 4. Цели 1. Дать пользователю прямое управление одним внешним value-бейджем marker. 2. Сделать выбор источника стабильным и не зависящим от порядка registry rows. 3. Обеспечить одинаковый результат на полном плане, статической карточке пространства и в preview редактора. 4. Исключить перекрытие нижнего value-бейджа и LQI. 5. Сохранить внешний вид существующих нетронутых конфигураций. 6. Не связывать выбор бейджа с влиянием устройства на комнатную температуру, свет, controls, Glow или заливку комнаты. ## 5. Не входит в задачу - несколько пользовательских value-бейджей у одного marker; - произвольное перетаскивание бейджа мышью; - ручной offset, размер шрифта, цвет, фон и рамка; - формулы, шаблоны, единицы пользователя, prefix/suffix; - изменение алгоритма режима «Значение вместо иконки»; - автоматическое предотвращение пересечения бейджа с соседними устройствами; - использование произвольных сложных JSON-атрибутов HA; - изменение комнатных подписей и метрик комнаты. ## 6. Термины - **Внешний value-бейдж** — компактная плашка рядом с marker, управляемая этой задачей. - **Внутреннее значение** — содержимое marker в режиме «Значение вместо иконки» (`display: value`). Это другая поверхность. - **Системный LQI** — существующая небоксированная подпись качества связи под marker, управляемая `show_signal` и настройкой пространства. - **Источник бейджа** — сохранённая конкретная HA-сущность, её поддерживаемый атрибут либо производное значение House Plan. - **Legacy auto** — существующая эвристика температуры/влажности для marker без новой явной настройки. ## 7. UX диалога устройства ### 7.1. Размещение Новый блок располагается в настройках устройства **после поля «Отображение» и его пояснения, непосредственно над предпросмотром**. В свёрнутом состоянии отображается одна строка: > `[ ] Отображать бейдж со значением` Если флаг включён, ниже раскрываются два поля: 1. **Значение** — выпадающий список доступных источников; 2. **Расположение** — выпадающий список: - Справа; - Снизу; - Слева; - Сверху. Порядок в списке расположений нормативный: `right`, `bottom`, `left`, `top`. По умолчанию используется **Справа**. ### 7.2. Поведение - Все изменения немедленно отражаются в существующем preview. - Preview показывает не условную схему, а тот же resolved badge, текст, позицию, unavailable-состояние и LQI-layout, что полный план и статическая карточка пространства. Сохранение для обновления preview не требуется. - Выключение чекбокса скрывает два поля, но сохраняет последний источник и позицию. Повторное включение восстанавливает их. - Если пользователь включает чекбокс впервые, рекомендуемый источник выбирается только в локальном draft и отражается в preview. Он записывается в config лишь потому, что включение checkbox является явным действием пользователя. Простое открытие диалога рекомендацию не материализует. Если источников нет, checkbox disabled и под ним показано: «У этого устройства нет доступных значений». - Если сохранённый источник временно отсутствует, чекбокс остаётся доступным, выпадающий список содержит отдельную выбранную строку «Источник недоступен» и предупреждение. Пользователь может сохранить настройку, отключить бейдж или выбрать другой источник. - При явной смене HA-привязки в том же диалоге источник старой привязки не переносится: выбирается рекомендация новой привязки либо бейдж отключается, если кандидатов нет. - В `display: static_icon` сохранённый чекбокс показывается disabled с объяснением: «Статичный значок не показывает живые значения». Настройка не удаляется и восстанавливается после возврата в динамический display mode. - Для скрытого marker preview продолжает показывать его реальный дизайн по существующему `designPreview`-контракту. ### 7.3. Текст строк списка Каждая опция должна показывать: - понятное имя показателя; - friendly name сущности, если одного имени показателя недостаточно; - текущее форматированное значение вторичной строкой; - entity id/attribute в tooltip либо вторичной технической строке. Примеры: - `Температура · Датчик климата` — `23,4 °C`; - `Текущая температура · Кондиционер` — `24,1 °C`; - `Заряд батареи · Датчик двери` — `78 %`; - `Состояние · Реле света` — `Включено`; - `Положение · Штора` — `35 %`; - `Качество Zigbee-сигнала (среднее)` — `142`. Технический entity id не должен заменять friendly name, но всегда должен быть доступен для диагностики. ## 8. Доступные источники ### 8.1. Общий принцип Dropdown строится только из источников, связанных с текущим marker: 1. активные сущности его `device:`/`entity:` binding; 2. прямые активные HA-сущности из `controls`; 3. прямые `marker:*` targets из `controls` как производное состояние; 4. производный собственный state пассивного/виртуального источника света; 5. производный средний LQI текущего устройства. Для `marker:*` бейдж не обходит controls/light graph самостоятельно: он потребляет уже разрешённое состояние существующего канонического resolver из #84. Второго обхода, собственного определения `on/off` и отдельного cycle resolver не создаётся. Произвольные сущности со всего Home Assistant не предлагаются. Это сохраняет смысл настройки «значение этого устройства» и не превращает dropdown в общий entity picker. ### 8.2. State сущности Допускается state активной сущности, если: - сущность принадлежит разрешённому набору выше; - её domain не `button` и не `event`; - `entity_category` не равен `config`; - state является строкой, числом или boolean либо временно отсутствует; - сущность не отключена в HA. Диагностические сущности допускаются: battery, power, energy, signal и другие измерения являются ценными кандидатами. `unknown`/`unavailable` не удаляют строку из списка — источник может восстановиться. ### 8.3. Поддерживаемые атрибуты Чтобы не показывать сотни служебных полей и не сохранять сложные объекты, разрешён централизованный allowlist скалярных атрибутов: | Domain | Атрибуты | |---|---| | `climate` | `current_temperature`, `temperature`, `current_humidity`, `humidity` | | `water_heater` | `current_temperature`, `temperature` | | `cover`, `valve` | `current_position` | | `fan` | `percentage` | | `humidifier` | `current_humidity`, `humidity` | | `light` | `brightness` | | `media_player` | `volume_level` | | `vacuum`, `lawn_mower` | `battery_level`, `fan_speed` | В список попадает только реально объявленный атрибут либо уже сохранённая ссылка на него. Allowlist хранится в одном frontend-модуле вместе с metadata: локализованное имя, единица/преобразование и порядок. Преобразования: - `brightness` 0–255 → 0–100%; - `volume_level` 0–1 → 0–100%; - `current_position`, `percentage`, humidity и `battery_level` → проценты; - температуры форматируются в единицах HA; - остальные значения используют HA formatter либо безопасный fallback. Произвольные строковые/массивные атрибуты (`entity_picture`, `hvac_modes`, `supported_features` и т. п.) не предлагаются. ### 8.4. Производные источники Поддерживаются два производных типа: - **Средний LQI** — тот же `lqiFor()`, который использует текущий системный индикатор; - **Состояние marker-источника** — итоговое `on/off` для пассивного источника или прямого `marker:*` target, вычисленное существующим light/control graph. Если внешний value-бейдж показывает средний LQI, отдельный системный LQI у этого marker не рисуется, чтобы не дублировать одно значение. ### 8.5. Рекомендация при первом включении Рекомендуемый источник выбирается детерминированно: 1. существующий legacy climate temperature при `use_climate_temp: true`; 2. существующий legacy temperature; 3. существующий legacy humidity; 4. `primary` state, если он допустим; 5. temperature; 6. humidity; 7. battery; 8. первый функциональный state по существующему role resolver; 9. первый прочий кандидат в стабильном порядке entity id; 10. средний LQI. Порядок реестра HA не используется как семантический приоритет. ## 9. Форматирование значения 1. Для state сначала вызывается `hass.formatEntityState()`. 2. Для атрибута сначала вызывается HA attribute formatter, если он доступен. 3. Если HA formatter отсутствует или не вернул пригодный текст, применяется нормативное преобразование §8.3. 4. Последний fallback — существующий безопасный scalar formatter House Plan; отдельный параллельный formatter для badge не вводится. 5. Суффикс и единица (`°C`, `°F`, `%` и т. п.) входят в готовый текст resolver. `device-face.ts` не приклеивает `°`/`%`: иначе при переводе legacy temperature/humidity на общий badge получится двойная единица. 6. `0`, `false`, `off`, `closed` являются валидными значениями и не скрывают бейдж. 7. `unknown`, `unavailable`, отсутствующий source и невалидный scalar дают видимый бейдж `—` с приглушённым unavailable-стилем и полным объяснением в tooltip/accessible label. Позиция при этом не прыгает. 8. Длинный текст сокращается многоточием. Полное локализованное значение остаётся в `title` и accessibility-описании. 9. Значение рендерится только как текст; HTML из HA не интерпретируется. ## 10. Взаимодействие с существующими режимами | Режим/настройка | Результат | |---|---| | Значок + динамическая подложка | Внешний бейдж показывается по настройке | | Значок + активность | Бейдж показывается; activity ring проходит позади него | | Значение вместо иконки | Внутреннее и внешнее значения независимы; допускаются разные источники | | Значение вместо иконки и тот же source | Допускается, preview показывает неблокирующее предупреждение о дублировании | | Всегда статичный значок | Внешний бейдж подавлен, настройка сохранена | | `live_states: false` | Явный бейдж продолжает показывать доступное HA-значение; статусная подложка остаётся нейтральной | | Явно выключенный бейдж | Legacy temperature/humidity для marker не показывается | | Явно включённый бейдж | Имеет приоритет над глобальным `show_temperature` | | Per-space `label_temp`, `label_hum`, `label_lqi`, `label_light` | Не подавляют явно настроенный value-badge: это свойство marker. Тумблеры продолжают управлять штатными room labels | | Показ системного LQI выключен на card/space | Системная строка LQI скрыта; явно выбранный value-badge, включая source LQI, остаётся видимым | | Источник badge = LQI | Показывается выбранный boxed badge; отдельный LQI скрыт | | Скрытый/HA-disabled marker | Marker и его бейдж не показываются по общему lifecycle-контракту | Выбор бейджа не изменяет статус устройства, activity, light role, Glow, controls, комнатные метрики и click action. ## 11. Раскладка ### 11.1. Якоря Бейдж привязывается к внешнему прямоугольнику `.dev`, а не к glyph и не к activity ring: - `right`: центр бейджа по вертикали, начало за правой гранью marker; - `left`: центр по вертикали, конец перед левой гранью; - `top`: центр по горизонтали, нижняя грань над marker; - `bottom`: центр по горизонтали, верхняя грань под marker. Зазор и размеры вычисляются от `--dev-size`, поэтому масштаб конкретного устройства, zoom, kiosk scale и статическая space-card дают одинаковые пропорции. Поворот glyph не поворачивает бейдж. ### 11.2. Конфликт с LQI Если position = `bottom` и системный LQI одновременно видим: 1. value-бейдж располагается первым, ближе к marker; 2. LQI располагается второй строкой ниже; 3. общий контейнер центрируется относительно marker; 4. между строками сохраняется масштабируемый gap; 5. ни один элемент не меняет выбранную пользователем сторону автоматически. При `top`, `left` и `right` системный LQI остаётся на штатном нижнем якоре. Если value source сам является LQI, отдельная строка LQI подавляется. ### 11.3. Другие пересечения - Activity ring может проходить под бейджем, но не сдвигает его и не вызывает layout jump. - Бейдж имеет `pointer-events: none`, поэтому не создаёт новую click target и не мешает hover/drag marker. - Автоматический flip у края плана запрещён: явно выбранная сторона стабильна. - Соседние marker и подписи комнат автоматически не перестраиваются. - Родительский слой не должен обрезать бейдж по прямоугольнику самого marker. ### 11.4. Safe area предпросмотра и текущая регрессия clipping В текущем `hp-device-preview` размер preview рассчитывается преимущественно по диаметру marker/activity ring, а `.previewstage` использует `overflow: hidden`. В результате существующий боковой temperature/humidity badge может быть слегка обрезан границей stage. Это подтверждённая регрессия и часть scope issue #90, а не допустимое ограничение preview. Новый preview обязан учитывать **полный визуальный bounding box** face: - marker plate; - activity ring; - внешний value-бейдж в выбранной позиции; - системный LQI; - нижний стек value badge + LQI; - HA-disabled/new-device служебные badges, если они присутствуют. Алгоритм fit/центрирования использует максимальные extents всех этих элементов и оставляет не менее одного масштабируемого gap до каждой границы previewstage. Нормативный browser-assert сравнивает `getBoundingClientRect()` бейджа и stage: `badge.left >= stage.left + gap`, `badge.right <= stage.right - gap`, аналогично по вертикали. Проверка выполняется для четырёх позиций, минимальной ширины диалога, максимального activity ring и legacy temperature/humidity badge. Нельзя исправлять clipping простым `overflow: visible`, если это позволяет бейджу залезть в facts-колонку или за скругление preview-карточки. Допустимы: 1. внутренний safe-area wrapper с рассчитанным padding; 2. вычисление fit по расширенному face bounding box; 3. комбинация обоих подходов. При смене right → bottom → left → top marker остаётся визуально центрированным в доступной области вместе со спутниками, без скачка размера stage. Длинное значение сначала сокращается до нормативной max-width, затем участвует в fit. Legacy temperature/humidity badge до явной настройки также получает этот fix. ## 12. Модель данных ### 12.1. TypeScript ```ts type ValueBadgePosition = 'right' | 'bottom' | 'left' | 'top'; type ValueBadgeSource = | { kind: 'entity_state'; entity_id: string } | { kind: 'entity_attribute'; entity_id: string; attribute: string } | { kind: 'derived_lqi' } | { kind: 'derived_marker_state'; ref: `marker:${string}` }; interface MarkerValueBadge { enabled: boolean; source?: ValueBadgeSource | null; position: ValueBadgePosition; } interface MarkerCfg { // ... value_badge?: MarkerValueBadge | null; } ``` `enabled: false` разрешает сохранять последний `source` и `position`. Для `enabled: true` отсутствие source является допустимым только как runtime состояние старого/повреждённого конфига; UI не создаёт такую запись. ### 12.2. Backend validation Backend семантически проверяет новую или изменяемую пользователем запись: - известный `kind`; - строковый `entity_id` формата HA; - attribute из frontend/backend общего allowlist; - канонический `ref` формата `marker:` для derived marker — тот же формат и те же helper/правила broken reference, что у `controls` (#84); - position из четырёх значений; - boolean `enabled`; - согласованность discriminated union. Произвольный attribute, пустой id/ref и несогласованная форма union отклоняются **на записи нового/изменённого badge**. Неизвестные соседние ключи в `value_badge` и `source` сохраняются (`ALLOW_EXTRA`) по downgrade-контракту `docs/CONFIG-COMPATIBILITY.md`: чтение и round-trip конфига из будущей версии не падают и не стирают неизвестные данные. ### 12.3. Ссылки, удаление и import remap `derived_marker_state.ref` является внутренней marker-ссылкой наравне с `controls[] = marker:`: - import/export #50 добавляет её в таблицу remap внутренних id; - при импорте пространства target id ремапится вместе с `marker.id`; - target вне импортируемого пространства снимается, badge становится `enabled: false`, position сохраняется, а import preview увеличивает счётчик отброшенных внешних marker-ссылок; - атомарная очистка/удаление target marker из #84 обрабатывает badge тем же общим helper: source сохраняется как broken/missing для runtime-диагностики, но не перепривязывается молча; - badge не добавляет ребро управления и не обходит граф, поэтому не создаёт отдельного цикла сверх уже проверенного `controls` graph. ## 13. Совместимость и миграция ### 13.1. Отсутствующее поле `value_badge == null/undefined` означает **legacy compatibility**, а не явное выключение: - renderer выполняет текущую temperature/humidity эвристику без изменения; - `show_temperature` продолжает управлять этим legacy-результатом; - существующие планы остаются pixel-identical до явного изменения настройки. ### 13.2. Диалог старого marker Для marker без `value_badge` UI строит эффективный draft: - если legacy badge сейчас существует — checkbox визуально включён, выбран тот же source, position = right; - иначе checkbox выключен, но dropdown заранее получает рекомендуемый source; - draft содержит внутренний `valueBadgeTouched = false`. Сохранение других полей не материализует новую настройку. `value_badge` записывается только после взаимодействия с checkbox/source/position. Это защищает конфигурацию от случайной миграции при временно неполном registry. ### 13.3. Явная настройка - `enabled: false` полностью подавляет legacy temperature/humidity этого marker независимо от `show_temperature`; - `enabled: true` показывает выбранный source независимо от `show_temperature`; - глобальное `show_temperature` сохраняется как compatibility-настройка для marker без явного `value_badge` и не удаляется из schema; - новые marker используют тот же draft/recommendation contract, но не получают произвольный generic badge без действия пользователя. ### 13.4. `use_climate_temp` Существующее поле сохраняет ответственность за участие climate `current_temperature` в средней температуре комнаты. - У marker без новой настройки оно также сохраняет старое поведение badge. - У явно настроенного marker оно **не управляет внешним badge**. - Текст настройки меняется с «Использовать датчик температуры устройства» на «Учитывать температуру устройства в комнате»; help объясняет, что внешний badge настраивается ниже отдельно. Так выбор показания не начинает неявно менять агрегаты комнаты и наоборот. ## 14. Runtime-архитектура ### 14.1. Один resolver Добавляется чистый `resolveDeviceValueBadge()` либо эквивалентный модуль, который получает: - `hass`/registry projection; - `DevItem`; - marker config; - effective display/global compatibility settings; - уже вычисленные presentation/light sources при необходимости. Результат: ```ts interface ResolvedValueBadge { configured: boolean; enabled: boolean; source: ValueBadgeSource | null; sourceLabel: string; text: string; fullText: string; position: ValueBadgePosition; availability: 'available' | 'unavailable' | 'missing'; isLqi: boolean; } ``` `ResolvedDevicePresentation` получает одно поле `valueBadge`. Старые `tempText`/`humText` перестают быть независимыми renderer decisions: legacy эвристика также преобразуется в этот единый результат. В одном face никогда не рендерятся два внешних value-бейджа. ### 14.2. Один renderer `device-face.ts` рендерит единый DOM: ```html 23,4 °C ``` Полный план, preview и static space-card используют тот же `ResolvedDevicePresentation`. Ни один renderer не выбирает source и не форматирует значение самостоятельно. `hp-device-preview` не имеет права повторно вычислять источник или позицию. Он получает готовый `valueBadge`, использует общий `renderDeviceFace()` и отдельно решает только задачу безопасного fit полного face bounding box (§11.4). ### 14.3. Кандидаты редактора Список кандидатов строится только при открытом диалоге и memoize-ится по: - binding; - active registry revision; - controls; - relevant state/attribute signature. Plan render не строит dropdown candidates. Новых подписок к HA не добавляется. ## 15. Lifecycle и edge cases | Ситуация | Нормативное поведение | |---|---| | Значение равно `0` | Показать `0` с единицей | | State `off`/`false` | Показать локализованное состояние | | `unknown`/`unavailable` | Стабильный бейдж `—`, unavailable style | | Сущность временно исчезла из `hass.states` | Бейдж `—`; source не переназначать | | Registry row удалён после сохранения | Бейдж `—`; editor показывает missing source | | HA-disabled source внутри активного device | Бейдж `—`; не подменять другой сущностью | | HA-disabled весь binding | Marker скрыт по lifecycle-контракту | | Смена friendly name | Label обновляется, source id сохраняется | | Смена unit system HA | Значение переформатируется без миграции config | | Source меняет numeric state на text | HA-formatted text показывается, если scalar | | Source начинает возвращать object/array | Бейдж `—`; HTML/JSON не выводить | | Обычное виртуальное устройство без HA binding, light role и controls | Checkbox disabled: нет кандидатов | | Пассивный forced-light marker без контроллеров | Доступно его каноническое derived marker state: по #84 источник имеет состояние всегда | | Виртуальный passive light с входящим controller | Доступно уже resolved derived marker state | | Несколько controls | Каждый прямой source — отдельная явная опция | | Target marker удалён | Сохранённый source missing, без silent fallback | | Controls graph изменён | Сохранённый source остаётся либо становится missing | | Marker size изменён | Бейдж масштабируется от `--dev-size` | | Glyph повёрнут | Бейдж остаётся горизонтальным | | Нижний badge + LQI | Стек: badge ближе, LQI ниже | | Badge source = LQI | Только boxed value badge, без дубликата LQI | | `display: value` | Внутреннее и внешнее значения независимы | | `display: static_icon` | Badge подавлен, config сохранён | | User-hidden marker в device editor | Ghost marker следует существующему контракту | | Импорт старой конфигурации | Поле отсутствует → legacy auto | | Экспорт/импорт новой конфигурации | Source и position round-trip без изменений | ## 16. Accessibility и touch - Бейдж не является отдельной интерактивной целью. - Accessible name marker дополняется: «{имя устройства}, {имя показателя}: {полное значение}». - Внутренний span скрывается от screen reader либо маркируется так, чтобы значение не читалось дважды. - `—` сопровождается текстом «значение недоступно», а не читается как необъяснимый символ. - Все label/select связаны программно; checkbox имеет описание причины disabled. - В режиме просмотра touch hit target остаётся marker, бейдж не перехватывает tap/pinch/pan. - Редактор на touch остаётся best-effort согласно общей политике проекта, но нативные select/checkbox должны быть доступны. ## 17. I18n и документация Новые строки добавляются синхронно в RU/EN: - `marker.value_badge.enabled`; - `marker.value_badge.source`; - `marker.value_badge.position`; - четыре позиции; - no candidates / unavailable / missing / duplicate notices; - названия allowlisted attributes и derived sources; - обновлённые label/help `use_climate_temp`. `` обязателен у checkbox включения, поля source и поля position. Для них добавляются пары `.help` и `.help.aria` в RU/EN по контракту #68; тест паритета help-реестра обязателен. Обновить: - `docs/USER-GUIDE.ru.md` и английский пользовательский документ; - `docs/ARCHITECTURE.md` — marker presentation/data model; - `docs/TESTING.md`; - пример marker config; - changelog значимой beta. ## 18. Безопасность и производительность - Все значения проходят текстовое Lit-binding; `unsafeHTML` запрещён. - Attribute source ограничен allowlist, backend повторяет проверку. - Runtime не сканирует весь HA registry для каждого marker. - Сохранённый source не вызывает service calls. - Изменение live value не должно пересоздавать marker DOM или запускать анимацию layout; меняется только текст/availability class. - Snapshot visual continuity обязан сохранять и badge, чтобы возврат на вкладку не давал промежуточный legacy/пустой кадр. - Фича укладывается в существующий large-house/performance профиль и его fail-closed сверку окружения; отдельный профиль и новый бюджет не заводятся. Существующие candidate/beta performance checks не ослабляются. ## 19. План реализации 1. Добавить типы, constants/allowlist и frontend/backend schema. 2. Обновить import/export и validation tests. 3. Реализовать candidate discovery, recommendation и formatter. 4. Реализовать `resolveDeviceValueBadge` и интегрировать в `ResolvedDevicePresentation`. 5. Перевести legacy temp/humidity projection на единый resolved badge. 6. Добавить draft/touched/save lifecycle в диалог marker. 7. Добавить UI и live preview. 8. Заменить `.tval/.hval` единым четырёхпозиционным CSS-компонентом и LQI stack. 9. Обновить static card, tooltip/accessibility и visual snapshot capture. 10. Добавить unit, backend, DOM smoke и golden fixtures. 11. Обновить документацию и выпустить через beta согласно promotion rule. ## 20. Тестовая матрица ### 20.1. Unit/frontend - четыре позиции; - source state: numeric/text/binary/zero/false; - каждый allowlisted attribute и преобразование percent/temperature; - unknown/unavailable/missing/non-scalar; - deterministic recommendation; - explicit on/off overrides legacy/global setting; - legacy marker остаётся pixel/semantic compatible; - bottom + LQI stack и LQI dedup; - static mode suppression with config preservation; - display=value independence; - hidden/disabled lifecycle; - binding change reset; - candidate filtering and stable ordering; - source ids survive rename/reorder. - explicit badge не зависит от `label_temp`/`label_hum`/`label_lqi`/`label_light`; - системный LQI продолжает зависеть от своих card/space toggles. ### 20.2. Backend/import-export - все valid union variants; - invalid kind/position/entity/attribute rejected на новой/изменённой записи; - unknown extra keys переживают read/round-trip; - enabled true without source rejected on write, но существующий повреждённый config читается без падения и даёт `—`; - `marker:` remap, external target drop/disable и import preview counter; - full/partial export-import round-trip; - old config without field accepted. ### 20.3. Browser/visual - preview = interactive plan = static card; - RU/EN long labels do not create horizontal dialog scroll; - изменение checkbox/source/position без сохранения немедленно обновляет preview; - marker scale 0.5/1/3; - icon rotation 0/137°; - activity ring behind badge; - all four positions in light/dark theme; - bottom badge with visible LQI; - legacy правый temperature/humidity badge целиком помещается в preview; - ни один из четырёх badge anchors не обрезается previewstage при минимальной ширине диалога и при activity ring максимального размера; - long value ellipsis/title; - touch pointer events do not steal marker click or pinch; - visual continuity after tab hide/restore. Golden fixtures должны содержать минимум один marker для каждой позиции и отдельный нижний badge + LQI. **Любой ненулевой diff существующих golden считается регрессом.** Новые эталоны принимаются только из полного Linux-артефакта CI по действующему HP-QA-01 контракту. ### 20.4. Мутационный гейт #85 Для каждого мутанта сохраняются исполнимый patch/команда и имя краснеющего теста: 1. Удалить touched gate, чтобы сохранение чужого поля материализовало `value_badge` → падает `marker value badge untouched save preserves legacy`. 2. Игнорировать `enabled: false` и вернуть legacy temperature/humidity → падает `explicit disabled value badge suppresses legacy metric`. 3. Заставить `hp-device-preview` вычислять source самостоятельно вместо `ResolvedDevicePresentation.valueBadge` → падает `device preview consumes resolved value badge verbatim`. 4. При missing source выбрать первый candidate → падает `missing value badge source never falls back silently`. 5. Не подавлять системный LQI при source `derived_lqi` → падает `derived lqi value badge renders exactly once`. 6. Убрать full-face safe gap/fit → browser-тест `preview value badge bounding box stays inside stage` падает хотя бы для right/left либо max activity ring. ## 21. Критерии приёмки 1. В диалоге над preview есть checkbox «Отображать бейдж со значением». 2. При включении доступны source и одна из четырёх позиций. 3. Выбранный source, а не иконка/порядок entities, определяет значение. 4. Full plan, preview и static card показывают одинаковый badge. 5. Checkbox, source и position обновляют preview немедленно, до сохранения. 6. Бейдж ни с одной стороны не обрезается в preview; текущая регрессия бокового legacy-бейджа исправлена. 7. Внешний badge не перекрывает системный LQI при нижней позиции. 8. Одновременно рендерится не более одного внешнего value-бейджа marker. 9. Missing/unavailable source не заменяется молча другим и показывает `—`. 10. Explicit off действительно убирает legacy temperature/humidity. 11. Explicit on работает независимо от глобального `show_temperature`. 12. Старые нетронутые markers выглядят как до изменения, кроме исправленного clipping в preview. 13. `static_icon` остаётся полностью статичным. 14. Badge не меняет свет, Glow, controls и комнатные агрегаты. 15. Config проходит backend validation и import/export round-trip. 16. RU/EN, keyboard, screen reader и touch-view контракты выполнены. 17. При открытии editor нативные source/position selectors показывают именно сохранённые effective values, даже если они не первые в динамическом списке; открытие не помечает `value_badge` как touched и не меняет config (#100). 18. Реализация выходит сначала в beta/RC. ## 22. Сложность, риски и оценка ### 22.1. Сложность **Средняя/выше средней.** Сам UI прост, но корректная функция затрагивает семантический projection, schema, три renderer surface, compatibility и registry lifecycle. Оценка инженерного объёма: | Блок | Оценка | |---|---:| | Модель, validation, import/export | 0,5–1 день | | Resolver/candidates/formatting/compatibility | 1–1,5 дня | | Dialog UX и preview | 0,5–1 день | | Legacy temp/humidity → единый resolver с pixel parity | 0,5–1 день | | Full-face preview fit и измеримый safe area | 0,5–1 день | | Renderer/CSS/LQI layout/a11y | 0,5–1 день | | Tests, golden, docs, beta hardening | 1–1,5 дня | | **Итого** | **4,5–8 рабочих дней, одна beta-итерация** | ### 22.2. Основные риски | Риск | Вероятность/ущерб | Снижение | |---|---|---| | Регрессия старых temp/humidity | средняя/высокий | absence = legacy, touched gate, parity fixtures | | Source исчезает после registry refresh | высокая/средний | stable ref, `—`, no silent fallback | | Перегруженный dropdown сложного устройства | средняя/средний | scope, grouping, allowlist, ordering | | Несовпадение plan/preview/static | средняя/высокий | единый presentation resolver и face renderer | | Перекрытия с LQI/activity | средняя/средний | нормативный stack и golden matrix | | Clipping спутников в узком preview | высокая/средний | full-face extents, safe area и browser matrix | | Скрытое изменение room average | низкая/высокий | полное разделение badge и climate aggregate | | Новая per-state работа ухудшит render | низкая/средний | memo/snapshot, candidates только в dialog | | Слишком длинные текстовые states | высокая/низкий | max-width, ellipsis, full accessible text | ## 23. Принятые продуктовые решения и вопросы Блокирующих вопросов для начала реализации нет. В этом ТЗ предложены следующие решения, которые должны считаться нормативными после принятия issue: 1. Один внешний пользовательский бейдж на marker. 2. Явная per-device настройка важнее глобального `show_temperature`. 3. Нетронутые marker остаются на legacy auto без принудительной миграции. 4. Системный LQI сохраняется; снизу элементы складываются в стек. 5. `static_icon` подавляет бейдж, не стирая настройку. 6. Неизвестное значение показывается как стабильный `—`, а не исчезает. 7. Выбор badge source не влияет на среднюю температуру комнаты. 8. Поддерживаются states связанных сущностей и ограниченный набор полезных scalar attributes; произвольные attributes не поддерживаются.