Files
houseplan-card/docs/specs/098-device-pulse-system.md
T

655 lines
37 KiB
Markdown
Raw 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 #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 актуальны.