Files
houseplan-card/legacy/specs/090-device-value-badge.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00

815 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<id>` для 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:<id>`:
- 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
<span class="value-badge pos-right available">23,4 °C</span>
```
Полный план, 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`.
`<hp-help>` обязателен у checkbox включения, поля source и поля position. Для
них добавляются пары `<key>.help` и `<key>.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:<id>` 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 не поддерживаются.