mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
655 lines
37 KiB
Markdown
655 lines
37 KiB
Markdown
# Issue #98 — единая система пульсаций и активности устройства
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/98
|
||
- **Статус ТЗ:** реализовано локально; ожидает prerelease/CI gate
|
||
- **Приоритет:** P1 / polish
|
||
- **Оценка:** сложность 7/10, пользовательская ценность 7/10, ценность для
|
||
разработки 8/10, score `(2×7+8)/7 = 3,14`
|
||
- **Риск:** средне-высокий — общий renderer, finite edge runtime, alarm safety
|
||
и parity трёх surfaces меняются одновременно
|
||
- **Область:** frontend presentation, runtime событий, общий renderer устройства,
|
||
editor preview, static card, i18n, accessibility, документация и QA
|
||
- **Модель данных:** без изменений
|
||
- **Связано:** #22 (pending/click feedback), #73 (visual continuity), #90
|
||
(value badge), #31 (полная View accessibility), `docs/LIGHT.md`,
|
||
`docs/ARCHITECTURE.md`
|
||
- **Touch:** View/kiosk полностью поддерживаются; device editor остаётся
|
||
desktop-first, его preview на touch — best effort по `docs/TOUCH-SUPPORT.md`
|
||
|
||
## 1. Резюме
|
||
|
||
House Plan должен использовать один понятный визуальный язык для динамики
|
||
устройства. Вокруг сохранённого marker допускаются ровно три вида
|
||
анимированной индикации:
|
||
|
||
1. **Тревога** — красная непрерывная пульсация, пока активно критическое
|
||
состояние.
|
||
2. **Короткое событие** — три конечные волны общей длительностью около 3,3 с.
|
||
3. **Постоянная активность** — спокойная непрерывная пульсация, пока устройство
|
||
действительно работает, перемещается или обнаруживает присутствие.
|
||
|
||
Статичных колец больше нет. При `prefers-reduced-motion: reduce` обычная
|
||
активность обозначается компактной сплошной точкой внутри подложки, а тревога
|
||
остаётся понятной по красной подложке и доступному описанию.
|
||
|
||
Семантические причины `event / presence / transition / running` сохраняются.
|
||
Они проецируются общим pure resolver в визуальные `none / alarm / short /
|
||
continuous`. Один renderer используется на интерактивном плане, в
|
||
`hp-device-preview` и в `houseplan-space-card`.
|
||
|
||
Хранимые значения `display`, `ripple_color` и `ripple_size` не меняются.
|
||
Старый `display: ripple` продолжает читаться как `icon_ripple`.
|
||
|
||
## 2. Проблема текущей реализации
|
||
|
||
Сейчас единая пользовательская идея реализована несколькими путями:
|
||
|
||
- `device-visual.ts` классифицирует semantic activity;
|
||
- `houseplan-card.ts` и `space-card.ts` отдельно держат finite runtime;
|
||
- `device-presentation.ts` скрывает обычную activity для части display modes;
|
||
- `device-face.ts` рисует `.activity-ring`;
|
||
- тревога рисуется отдельно через `.dev.alarm::after`;
|
||
- presence является статичным кольцом;
|
||
- reduced motion превращает все activity-эффекты в статичное кольцо;
|
||
- preview умеет показать только один абстрактный event-demo;
|
||
- UI называет параметры «Цвет/Размер эффекта», не объясняя их область действия.
|
||
|
||
Из-за этого визуальная система расходится по типам устройств и surfaces.
|
||
Пользователь не может предсказать результат выбранной опции, а новый класс
|
||
устройства приходится вручную подключать к нескольким renderer/CSS-веткам.
|
||
|
||
## 3. Цели
|
||
|
||
1. Один нормативный pipeline от HA state до визуального pulse.
|
||
2. Ровно три динамических pulse-kind без статичных колец.
|
||
3. Одинаковый результат на полном плане, в preview и static card.
|
||
4. Чёткие названия display modes и контекстные подсказки.
|
||
5. Предсказуемые приоритеты alarm, short event и continuous activity.
|
||
6. Безопасный reduced-motion fallback без мигающих/движущихся колец.
|
||
7. Полная совместимость существующей конфигурации.
|
||
8. Детерминированные runtime, smoke и golden проверки.
|
||
|
||
## 4. Не входит в задачу
|
||
|
||
- новые правила выбора `primary`, binding или visual sources;
|
||
- новые классы устройств сверх уже распознаваемой семантики;
|
||
- optimistic/pending feedback между кликом и ответом HA — #22;
|
||
- изменение Glow, лучей из окон, room fill и light-source resolver;
|
||
- анимация live vacuum puck и его следа;
|
||
- boot/recovery/navigation animations;
|
||
- пользовательская настройка скорости pulse;
|
||
- изменение config schema и переименование `ripple_*` в storage;
|
||
- автоматическое включение activity mode у существующих marker;
|
||
- отдельные цвета/размеры для presence, transition и running.
|
||
|
||
## 5. Термины и нормативные типы
|
||
|
||
### 5.1. Semantic activity
|
||
|
||
Существующий тип остаётся источником причины:
|
||
|
||
```ts
|
||
type DeviceActivity =
|
||
| 'none'
|
||
| 'event'
|
||
| 'presence'
|
||
| 'transition'
|
||
| 'running';
|
||
```
|
||
|
||
- `event` — наблюдённый короткий edge;
|
||
- `presence` — устойчивое обнаружение присутствия;
|
||
- `transition` — фактическое механическое движение либо короткий terminal
|
||
fallback;
|
||
- `running` — устойчивая полезная работа.
|
||
|
||
### 5.2. Rendered pulse
|
||
|
||
Вводится визуальная проекция:
|
||
|
||
```ts
|
||
type DevicePulseKind = 'none' | 'alarm' | 'short' | 'continuous';
|
||
|
||
type DevicePulseReason =
|
||
| 'alarm'
|
||
| 'event'
|
||
| 'presence'
|
||
| 'transition'
|
||
| 'running';
|
||
|
||
interface ResolvedDevicePulse {
|
||
kind: DevicePulseKind;
|
||
reason: DevicePulseReason | null;
|
||
generation: number;
|
||
color: string | null;
|
||
diameterScale: number;
|
||
expiresAt: number | null;
|
||
reducedMotionIndicator: 'none' | 'dot';
|
||
}
|
||
```
|
||
|
||
`kind` определяет DOM/CSS. `reason` сохраняется для preview, diagnostics,
|
||
accessible description и будущих расширений, но не создаёт четвёртый вид
|
||
анимации. `generation` и `expiresAt` имеют смысл только для `short`:
|
||
continuous/alarm не должны перезапускать CSS timeline из-за обычного HA
|
||
re-render. `diameterScale` назван отдельно от масштаба самого marker и означает
|
||
максимальный диаметр волны в диаметрах подложки.
|
||
|
||
## 6. Источники семантики
|
||
|
||
### 6.1. Тревога
|
||
|
||
Тревога активна, пока хотя бы один разрешённый critical source имеет
|
||
критическое состояние:
|
||
|
||
- `binary_sensor` device class: `smoke`, `gas`, `carbon_monoxide`, `moisture`,
|
||
`safety`, `tamper`, `problem`;
|
||
- `siren` в активном состоянии;
|
||
- `alarm_control_panel: triggered`;
|
||
- остальные случаи, уже канонически возвращаемые `isAlarmState()`.
|
||
|
||
ТЗ не расширяет `isAlarmState()`. Любое последующее расширение делается в
|
||
семантическом resolver и автоматически получает общий alarm pulse.
|
||
|
||
### 6.2. Короткое событие
|
||
|
||
Short pulse запускается только от наблюдённого edge после установленного
|
||
baseline:
|
||
|
||
| Источник | Условие |
|
||
| --- | --- |
|
||
| motion/vibration/sound | валидный `off → on` |
|
||
| door/window/garage/opening contact | валидный `off → on` |
|
||
| button/event | изменение валидного state/event token |
|
||
| stateless action House Plan | подтверждённый успешный service call |
|
||
| cover | прямой `closed ↔ open` без наблюдаемого opening/closing |
|
||
| lock | прямой `locked ↔ unlocked` без locking/unlocking |
|
||
| valve | прямой `closed ↔ open` без opening/closing |
|
||
|
||
Если в одном HA snapshot обнаружены и ordinary event, и terminal fallback,
|
||
выбирается `event`: порядок обхода сущностей и registry rows не влияет на
|
||
результат. Между разными snapshot более новый валидный short edge перезапускает
|
||
цикл целиком.
|
||
|
||
Событие не создаётся:
|
||
|
||
- на первом snapshot;
|
||
- при `unknown/unavailable/missing → value`;
|
||
- при reconnect/resume/rebuild source graph;
|
||
- при неуспешном, отменённом или неподдерживаемом service call;
|
||
- от одного только hover, focus, preview re-render или смены display mode.
|
||
|
||
### 6.3. Постоянная активность
|
||
|
||
Continuous pulse существует ровно пока semantic state активен:
|
||
|
||
| Причина | Примеры |
|
||
| --- | --- |
|
||
| presence | `occupancy/presence = on` |
|
||
| transition | cover/lock/valve `opening/closing/locking/unlocking`; moving sensor; vacuum returning |
|
||
| running | light/switch/fan/humidifier `on`; running/power binary sensor; climate heating/cooling/preheating/defrosting; vacuum cleaning; script on; распознанные working states техники |
|
||
| controls | хотя бы один выбранный canonical visual/control source реально работает |
|
||
|
||
Ограничения:
|
||
|
||
- `automation: on` означает enabled, а не running;
|
||
- `media_player` не получает continuous pulse от `on/playing/paused`;
|
||
- `cover` open/closed остаётся glyph-state, а не working status;
|
||
- `unavailable/unknown` снимает обычную activity;
|
||
- transition заканчивается сразу при terminal state, без доведения до 3,3 с;
|
||
- terminal fallback, наоборот, является short pulse и ограничен finite runtime.
|
||
|
||
## 7. Приоритеты и projection
|
||
|
||
Нормативный порядок:
|
||
|
||
1. удалённый/effective hidden marker не рендерится;
|
||
2. HA-disabled design preview и orphaned binding не имеют pulse;
|
||
3. `static_icon` возвращает `none` даже при alarm;
|
||
4. alarm возвращает `alarm` и перекрывает всё;
|
||
5. если `live_states === false`, обычный pulse отсутствует;
|
||
6. finite event runtime возвращает `short`;
|
||
7. актуальная `presence/transition/running` возвращает `continuous`;
|
||
8. иначе `none`.
|
||
|
||
`unavailable` не является device-level veto само по себе. Resolver сначала
|
||
отбрасывает недоступные samples: активный critical source другого entity всё
|
||
ещё обязан показать alarm, а доступный рабочий source — continuous. Pulse
|
||
отсутствует из-за availability только когда после фильтрации не осталось ни
|
||
одного доступного релевантного source. Это сохраняет действующий контракт
|
||
`combineVisualSamples()` и не позволяет одному диагностическому entity погасить
|
||
реальную тревогу устройства.
|
||
|
||
Short pulse имеет приоритет над continuous. После окончания short resolver
|
||
снова проверяет актуальную семантику и без паузы возвращает continuous, если
|
||
устройство продолжает работать.
|
||
|
||
Если во время terminal fallback появляется реальный transition state, finite
|
||
fallback сбрасывается и сразу заменяется continuous transition. При первом
|
||
входе в alarm ordinary finite runtime **очищается**, а текущие samples становятся
|
||
новым baseline. Замораживание запрещено: после снятия тревоги старый short не
|
||
возобновляется, но новый валидный edge, произошедший уже после alarm,
|
||
обрабатывается обычно.
|
||
|
||
## 8. Матрица display modes
|
||
|
||
Хранимые токены не меняются. `badge` остаётся default.
|
||
|
||
| Config | RU | EN |
|
||
| --- | --- | --- |
|
||
| `badge` | Значок + состояние | Icon + state |
|
||
| `icon_ripple` | Значок + состояние и активность | Icon + state and activity |
|
||
| `value` | Значение + состояние | Value + state |
|
||
| `static_icon` | Всегда статичный значок | Always static icon |
|
||
|
||
### 8.1. Полная матрица
|
||
|
||
| Режим | Нейтрально / unavailable | Working/open state | Short | Continuous | Alarm | Satellites |
|
||
| --- | --- | --- | --- | --- | --- | --- |
|
||
| Значок + состояние | текущая тёмная/приглушённая подложка | жёлтая working, оранжевая physical open; cover меняет glyph | нет | нет | красная подложка + alarm pulse | icon/morph, value badge и LQI разрешены |
|
||
| Значок + состояние и активность | как выше | как выше | три волны | единая спокойная пульсация | красная подложка + alarm pulse | как выше |
|
||
| Значение + состояние | динамическая подложка | жёлтая/оранжевая подложка | нет | нет | красная подложка + alarm pulse | value вместо icon, fallback icon, value badge/LQI |
|
||
| Всегда статичный значок | всегда нейтральная тёмная подложка | без изменений | нет | нет | нет | только базовый icon; без morph/value/value badge/LQI/RGB/live-vacuum overlays |
|
||
|
||
Alarm является safety-исключением для всех state-aware modes. Только явный
|
||
`static_icon` скрывает alarm presentation; диалог сохраняет предупреждение для
|
||
alarm-capable binding.
|
||
|
||
Hover, focus outline, shadow и click action не входят в display projection и
|
||
остаются одинаковыми по действующему контракту.
|
||
|
||
## 9. Визуальный контракт
|
||
|
||
### 9.1. Alarm
|
||
|
||
- системный цвет `#f25a4a` либо единый design token с тем же контрастом;
|
||
- красная plate сохраняется;
|
||
- один expanding/fading pulse, непрерывно пока alarm активен;
|
||
- пользовательские `ripple_color/ripple_size` игнорируются;
|
||
- интенсивность не усиливается при hover;
|
||
- частота/opacity не создают WCAG flashing violation.
|
||
|
||
### 9.2. Short
|
||
|
||
- ровно три последовательные волны;
|
||
- одна волна 1,1 с, total runtime 3,3 с;
|
||
- повторный edge увеличивает generation и перезапускает весь цикл;
|
||
- старый DOM/timer не накапливается;
|
||
- цвет и максимальный диаметр берутся из ordinary activity settings.
|
||
|
||
### 9.3. Continuous
|
||
|
||
- один breathing pulse без статичной фазы;
|
||
- один общий tempo для presence, transition и running: рекомендуемо 2,4 с
|
||
`ease-in-out infinite`;
|
||
- reason не меняет геометрию, скорость и интенсивность;
|
||
- выключение/idle/terminal state снимает pulse немедленно;
|
||
- смена причины `presence → running` не должна визуально вспыхивать или
|
||
перезапускать timeline, если kind остаётся `continuous`.
|
||
|
||
### 9.4. Цвет и размер ordinary activity
|
||
|
||
Порядок цвета:
|
||
|
||
1. валидный `marker.ripple_color`;
|
||
2. текущий RGB canonical light source;
|
||
3. `--hp-accent`.
|
||
|
||
Размер: `marker.ripple_size`, default `3`, текущие backend limits сохраняются.
|
||
Параметры применяются только к `short/continuous`, не к alarm и не к
|
||
reduced-motion dot.
|
||
|
||
## 10. Reduced motion и accessibility
|
||
|
||
При `prefers-reduced-motion: reduce`:
|
||
|
||
- `.device-pulse` с движущимися кольцами не рендерится либо полностью скрыт;
|
||
- alarm читается по красной plate и accessible state;
|
||
- working/open читаются по существующим plate/glyph states;
|
||
- в `icon_ripple` short/continuous отображаются `.activity-dot`;
|
||
- short dot живёт то же finite окно 3,3 с;
|
||
- continuous dot существует до окончания semantic state;
|
||
- у других display modes dot отсутствует;
|
||
- статичное outline/ring запрещено.
|
||
|
||
Dot находится **внутри нижнего правого угла plate**, а не снаружи marker:
|
||
|
||
- не конфликтует с `new-device` dot сверху справа;
|
||
- не увеличивает bounding box;
|
||
- не пересекается с внешним value badge и нижним LQI;
|
||
- имеет контрастную 1 px обводку через plate/background token;
|
||
- не является интерактивной целью и имеет `aria-hidden="true"`.
|
||
|
||
Доступное имя/описание marker включает локализованную причину:
|
||
|
||
- alarm: «Тревога»;
|
||
- event: «Короткое событие»;
|
||
- presence: «Обнаружено присутствие»;
|
||
- transition: «Устройство перемещается/переключается»;
|
||
- running: «Устройство работает».
|
||
|
||
Визуальный эффект не должен быть единственным носителем критической
|
||
информации. В рамках #98 обновляется существующее `aria-label` полного marker и
|
||
`role="img"` preview; static card сохраняет read-only семантику. Добавление
|
||
полной клавиатурной навигации по объектам плана, нового tab order или
|
||
`role="button"` относится к #31 и не должно скрыто появиться в этой задаче.
|
||
|
||
Для alarm кроме motion всегда остаются красная plate и локализованный текст в
|
||
доступном имени/информационной поверхности. Для short/continuous отсутствие
|
||
анимации при reduced motion не меняет действие по нажатию и hit area.
|
||
|
||
## 11. UI настроек устройства
|
||
|
||
### 11.1. Названия и подсказки
|
||
|
||
Один общий `marker.display_hint` заменяется подсказкой для текущего mode.
|
||
|
||
Нормативный смысл:
|
||
|
||
- **Значок + состояние:** plate и icon показывают состояние без обычной
|
||
пульсации; критическая тревога пульсирует красным.
|
||
- **Значок + состояние и активность:** дополнительно показывает короткие
|
||
события и непрерывную реальную активность.
|
||
- **Значение + состояние:** выводит однозначное HA value, при невозможности —
|
||
state-aware icon; alarm остаётся видимым.
|
||
- **Всегда статичный значок:** marker не реагирует на HA state; hover, focus и
|
||
click продолжают работать.
|
||
|
||
Поля переименовать:
|
||
|
||
- RU: «Цвет пульсации активности», «Размер пульсации активности»;
|
||
- EN: `Activity pulse color`, `Activity pulse size`.
|
||
|
||
Под полями: «Не влияет на красную тревогу» / `Does not affect red alarms`.
|
||
|
||
Поля показываются только при `icon_ripple`. Их сохранённые значения не
|
||
стираются при временном выборе другого display mode и применяются снова после
|
||
возврата.
|
||
|
||
### 11.2. Preview
|
||
|
||
`hp-device-preview` получает готовый `ResolvedDevicePulse`, а не вычисляет
|
||
activity заново.
|
||
|
||
В режиме `icon_ripple` доступны две независимые preview-команды:
|
||
|
||
1. **Показать короткую пульсацию** — запускает local-only short demo на 3,3 с.
|
||
2. **Показать постоянную пульсацию** — включает continuous demo до повторного
|
||
нажатия, смены binding/display, появления real pulse или закрытия preview.
|
||
|
||
Правила:
|
||
|
||
- demo не меняет HA state, config и основной `_activityRt`;
|
||
- real alarm/short/continuous имеет приоритет и отключает demo;
|
||
- одновременно активен максимум один demo;
|
||
- кнопка continuous меняет label на «Остановить постоянную пульсацию»;
|
||
- reduced motion показывает dot и пояснение, а не кольцо;
|
||
- preview facts различают reason, даже если rendered kind один;
|
||
- fit учитывает максимальный ordinary pulse, alarm pulse и satellites без
|
||
clipping.
|
||
|
||
## 12. Архитектура
|
||
|
||
### 12.1. Нормативный pipeline
|
||
|
||
```text
|
||
HA states + registry
|
||
→ entityVisualSample(s)
|
||
→ combineVisualSamples
|
||
→ per-card finite edge runtime
|
||
→ resolveDevicePresentation
|
||
→ resolveDevicePulse
|
||
→ renderDeviceFace/device-pulse
|
||
→ interactive plan | preview | static card
|
||
```
|
||
|
||
Рекомендуется отдельный pure module `src/device-pulse.ts`:
|
||
|
||
```ts
|
||
resolveDevicePulse({
|
||
display,
|
||
visual,
|
||
liveStates,
|
||
effectiveHidden,
|
||
bindingStatus,
|
||
runtime,
|
||
rippleColor,
|
||
rippleScale,
|
||
reducedMotion,
|
||
}): ResolvedDevicePulse
|
||
```
|
||
|
||
Resolver не читает DOM, timers, `matchMedia` и global state. `now` передаётся
|
||
явно через runtime/presentation options для детерминированных тестов.
|
||
|
||
Полный `ResolvedDevicePresentation` может содержать pulse как вложенную
|
||
проекцию, но не должен одновременно оставлять второй нормативный
|
||
`activity + classes` path. На время одного beta допускается compatibility
|
||
adapter старых классов; бизнес-решение всегда принимается до renderer.
|
||
|
||
### 12.2. Runtime ownership
|
||
|
||
Finite runtime остаётся per card instance:
|
||
|
||
- interactive card и static card не делят Map;
|
||
- Map key — стабильный marker id;
|
||
- отдельная `activitySourceSignature` включает binding и только
|
||
visual/critical sources, которые способны породить semantic edge;
|
||
- signature **не включает** display mode, value/value-badge source, LQI,
|
||
temperature visibility, satellites, icon или preview-only presentation;
|
||
- source signature change устанавливает новый baseline;
|
||
- runtime хранит `startedAt`, `expiresAt`, semantic reason и generation;
|
||
- один timeout только будит render около expiry; истина определяется `now <
|
||
expiresAt`, а не фактом срабатывания timer;
|
||
- disconnect очищает timers/Map;
|
||
- hidden tab не проигрывает накопившийся short после возврата.
|
||
|
||
Текущую `presentationSourceSignature()`, которая также включает value source,
|
||
нельзя без проверки объявить activity signature: смена бейджа или глобального
|
||
`show_temperature` не должна отменять либо искусственно перезапускать pulse.
|
||
Допустимо сохранить старую функцию для memo/snapshot и добавить узкую
|
||
`activitySourceSignature()` рядом с semantic resolver.
|
||
|
||
`prefers-reduced-motion` подписывается один раз на surface/card host, а не на
|
||
каждый marker. Host передаёт актуальный boolean в pure resolver и вызывает один
|
||
re-render при изменении media query. Full card, static card и standalone
|
||
preview самостоятельно владеют подпиской и очищают её при disconnect.
|
||
|
||
Можно сохранить совместимость текущего `flashTs/flashKind/gen`, если наружный
|
||
контракт даёт те же гарантии. Переименование runtime fields не является целью.
|
||
|
||
### 12.3. Один renderer
|
||
|
||
`renderDeviceFace()` рендерит один слой:
|
||
|
||
```html
|
||
<span class="device-pulse pulse-short reason-event gen-2" aria-hidden="true">
|
||
<i></i><i></i><i></i>
|
||
</span>
|
||
```
|
||
|
||
или continuous/alarm variant. Reduced-motion dot создаётся тем же renderer.
|
||
|
||
Удалить:
|
||
|
||
- `.dev.alarm::after` как отдельный pulse path;
|
||
- статичную `.activity-ring.presence` ветку;
|
||
- reduced-motion static ring;
|
||
- surface-specific самостоятельное решение вида pulse.
|
||
|
||
Класс `.dev.alarm` сохраняется только для alarm plate. Внешние
|
||
`activity-event/presence/transition/running` classes можно оставить временно
|
||
для compatibility hooks на один beta-cycle, но production CSS и новые тесты не
|
||
должны от них зависеть. Перед stable их судьба фиксируется в реестре
|
||
compatibility #33: удалить либо документировать как deprecated styling hooks.
|
||
|
||
## 13. Сохранение и обратная совместимость
|
||
|
||
- `display` tokens не меняются;
|
||
- `ripple` продолжает нормализоваться в `icon_ripple`;
|
||
- `ripple_color` и `ripple_size` читаются/пишутся как раньше;
|
||
- Open → Save без изменения display не материализует новые поля;
|
||
- импорт/экспорт round-trip побайтно сохраняет эти поля;
|
||
- backend validation limits не меняются;
|
||
- downgrade видит прежние поля и может показать старый ring — это ожидаемая
|
||
визуальная разница, не потеря данных;
|
||
- schema/model migration не требуется.
|
||
|
||
## 14. Эдж-кейсы
|
||
|
||
- несколько sources одновременно: alarm > newest short > continuous;
|
||
- short приходит во время continuous и после себя возвращает актуальный
|
||
continuous;
|
||
- два short подряд перезапускают три волны через generation;
|
||
- alarm возникает во время short и полностью его подавляет;
|
||
- alarm снимается после expiry short — старый short не возвращается;
|
||
- alarm onset очищает finite runtime и синхронизирует baseline всех sources;
|
||
- binding/source graph изменился — старые edges не переносятся;
|
||
- смена display, value badge, LQI и `show_temperature` не меняет activity
|
||
signature и не создаёт edge;
|
||
- `unavailable → on` после reconnect — baseline без события;
|
||
- один unavailable sample не гасит доступный alarm/continuous другого sample;
|
||
- быстрый `opening → open` прекращает continuous сразу;
|
||
- прямой `closed → open` запускает short fallback;
|
||
- custom color невалиден — безопасный fallback, без CSS injection;
|
||
- `ripple_size` min/max, marker scale 0.5/3, value badge на четырёх сторонах;
|
||
- hover/focus не меняет opacity/speed и не перезапускает animation;
|
||
- `static_icon → icon_ripple` показывает только актуальную continuous
|
||
семантику, но не старый finite event;
|
||
- live_states on/off применяется немедленно без Save;
|
||
- preview draft не мутирует runtime полного плана;
|
||
- две карточки одного плана имеют независимый generation/timer;
|
||
- reduced-motion меняется во время активного effect без remount;
|
||
- media-query listener существует один раз на surface, а не на marker;
|
||
- HA-disabled preview ghost не получает alarm/activity dot;
|
||
- new-device dot и activity dot видны раздельно;
|
||
- theme/contrast и пользовательский accent;
|
||
- touch hover отсутствует, click target не увеличивается pulse layer.
|
||
- background/resume просрочивает short по `expiresAt` до первого нового paint;
|
||
замороженная браузером CSS-анимация не продолжает старые три волны.
|
||
|
||
## 15. Производительность и безопасность
|
||
|
||
- один pure pulse resolve на уже построенную presentation;
|
||
- не повторять `resolvedLightSources` ради цвета внутри каждого surface;
|
||
- continuous смена HA state не создаёт новые DOM nodes каждый tick;
|
||
- максимум один finite timeout на marker с short runtime;
|
||
- CSS animation использует transform/opacity, не layout properties;
|
||
- pulse layer `pointer-events: none`;
|
||
- color проходит существующую safe color normalization;
|
||
- нет HTML из HA state/labels;
|
||
- golden/performance fixture не должен получить дополнительный full-plan
|
||
rebuild при каждом animation frame.
|
||
|
||
## 16. Тестирование
|
||
|
||
### 16.1. Unit
|
||
|
||
Обязательная таблица `semantic → pulse`:
|
||
|
||
- все display modes × alarm/event/presence/transition/running/none;
|
||
- alarm > short > continuous;
|
||
- live_states false;
|
||
- static_icon;
|
||
- hidden, HA-disabled, orphaned/unavailable;
|
||
- terminal fallback и замена real transition;
|
||
- initial/reconnect/unavailable baseline;
|
||
- rapid retrigger/generation;
|
||
- source signature reset;
|
||
- activity signature не зависит от display/value badge/LQI/temperature UI;
|
||
- один unavailable source рядом с доступным alarm/running source;
|
||
- alarm onset baseline reset и отсутствие stale short после alarm;
|
||
- custom/fallback color и scale;
|
||
- reduced-motion dot/none, hot media-query change, никогда static ring.
|
||
|
||
### 16.2. Browser smoke
|
||
|
||
- motion, contact, button/event, presence, working switch, climate, cover,
|
||
lock, valve, vacuum, script, alarm;
|
||
- четыре display modes по полной матрице;
|
||
- short три волны и cleanup после 3,3 с;
|
||
- continuous появляется/исчезает по state;
|
||
- hover не перезапускает timeline;
|
||
- preview parity и обе demo-команды;
|
||
- static card parity;
|
||
- runtime независим между двумя card instances;
|
||
- hide/resume после истёкшего short не продолжает замороженную CSS-анимацию;
|
||
- reduced-motion dot geometry и absence of rings;
|
||
- спутники marker не перекрываются.
|
||
|
||
Таймеры тестируются управляемым `now`/fake clock; CI не ждёт реальные 3,3 с
|
||
там, где достаточно pure assertion.
|
||
|
||
### 16.3. Golden
|
||
|
||
Минимальная матрица:
|
||
|
||
- desktop/mobile;
|
||
- light/dark;
|
||
- alarm;
|
||
- short в фиксированном progress;
|
||
- continuous;
|
||
- reduced-motion dot;
|
||
- четыре display modes;
|
||
- presence без статичного кольца;
|
||
- max ripple scale + value badge/LQI/new-device dot.
|
||
|
||
Новые golden принимаются только из Linux CI artifact по HP-QA-01.
|
||
|
||
## 17. Документация и release-артефакты
|
||
|
||
В той же реализации обновить:
|
||
|
||
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #98;
|
||
- `docs/ARCHITECTURE.md` — presentation/pulse pipeline;
|
||
- `docs/LIGHT.md` — явно отделить marker pulse от Glow;
|
||
- `docs/FILTERING.md` — semantic sources/lifecycle;
|
||
- пользовательскую документацию по четырём display modes;
|
||
- `docs/TESTING.md` — unit/smoke/golden матрицу;
|
||
- ru/en i18n parity.
|
||
|
||
Release body: значимое пользовательское изменение одной строкой + ссылки на
|
||
RU/EN changelog. Поставка сначала в beta. Full golden/performance gate — перед
|
||
stable release согласно действующей release policy.
|
||
|
||
## 18. План реализации
|
||
|
||
1. Ввести `activitySourceSignature`, pure `ResolvedDevicePulse` и
|
||
исчерпывающие unit tests.
|
||
2. Подключить pulse к `ResolvedDevicePresentation` без изменения DOM и
|
||
добавить одну reduced-motion subscription на surface.
|
||
3. Перевести `renderDeviceFace()` на единый `device-pulse` renderer.
|
||
4. Удалить alarm pseudo-element, presence static ring и reduced-motion ring.
|
||
5. Нормализовать finite runtime interactive/static cards.
|
||
6. Обновить display copy и activity settings.
|
||
7. Реализовать две preview demo-команды.
|
||
8. Обновить accessibility descriptions и satellite layout.
|
||
9. Добавить browser/golden coverage и документацию.
|
||
10. Выпустить beta, проверить реальные motion/presence/alarm/cover fixtures.
|
||
|
||
## 19. Критерии приёмки
|
||
|
||
- [ ] В production есть один pure pulse resolver и один DOM renderer.
|
||
- [ ] Визуальных pulse-kind ровно три: alarm, short, continuous.
|
||
- [ ] Статичных колец нет в normal, reduced motion и preview.
|
||
- [ ] Presence показывает continuous pulse, а не кольцо.
|
||
- [ ] Real transition существует ровно пока идёт переход; terminal fallback —
|
||
short около 3,3 с.
|
||
- [ ] Alarm использует единый renderer, системный красный и имеет высший
|
||
приоритет.
|
||
- [ ] Матрица четырёх display modes соблюдается на всех surfaces.
|
||
- [ ] `live_states: false` подавляет ordinary activity, но не alarm.
|
||
- [ ] `static_icon` не показывает alarm/activity и сохраняет hover/focus/click.
|
||
- [ ] Цвет/размер ordinary activity не влияют на alarm.
|
||
- [ ] Reduced motion использует внутреннюю dot только для activity mode.
|
||
- [ ] Preview показывает фактический результат и две безопасные demo-команды.
|
||
- [ ] Первый snapshot/reconnect/unavailable recovery не создают false event.
|
||
- [ ] Activity signature не зависит от value/satellite/display presentation.
|
||
- [ ] Alarm onset очищает finite runtime; один unavailable source не подавляет
|
||
доступную тревогу другого source.
|
||
- [ ] Rapid retrigger не накапливает DOM/timers и корректно перезапускает цикл.
|
||
- [ ] Background/resume не продолжает просроченную short-анимацию.
|
||
- [ ] Модель, backend и существующие конфиги остаются совместимыми.
|
||
- [ ] Full plan, preview и static card дают одинаковую projection.
|
||
- [ ] ru/en, docs, unit, smoke и golden gates актуальны.
|