Files
houseplan-card/docs/specs/381-no-op-tap-action.md
T
2026-08-30 07:48:16 +00:00

302 lines
23 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 #381 — действие по нажатию «Ничего не делать»
- **Issue:** https://github.com/Matysh/houseplan-card/issues/381
- **Статус документа:** утверждено, реализовано, готово к code review
- **Приоритет / тип:** P2 · feature / polish
- **Область:** marker config, Редактор устройств, View/киоск, frontend action
projection, backend validation, i18n, документация и QA
- **Связи:** #94 (универсальный Toggle), #178 (точная Toggle-сущность)
- **Ревизия:** 2 (2026-08-30)
## Сценарий
Администратор дома размещает на плане датчик или виртуальный маркер, который
должен только показывать место и состояние. В Редакторе устройств он выбирает
для маркера действие **«Ничего не делать»** и сохраняет настройки. После этого
член семьи, гость или киоск может коротко нажать маркер, не открывая карточку и
не отправляя команду устройству. Долгое нажатие и правый клик сохраняют свои
отдельные информационные действия.
## Что человек увидит до и после
**До:** короткое нажатие всегда открывает информационную поверхность либо
пытается выполнить команду. **После:** в селекторе появляется явный вариант
«Ничего не делать», и короткое нажатие выбранного маркера не даёт никакого
результата, пока сам маркер продолжает показывать состояние на плане.
## Проблема
`TAP_ACTIONS` предлагает только `info`, `more-info`, `toggle` и `run`.
`projectedTapAction()` проецирует неизвестный сохранённый токен в `info`, а
`_clickDevice()` после известных команд открывает локальную карточку. Backend
`MARKER_SCHEMA` также принимает только эти четыре значения, legacy `cover` и
`null`. Поэтому строка в селекторе без сквозного контракта либо будет отклонена
сервером, либо после reload превратится в открытие карточки.
Использовать неработоспособный `toggle` как замену нельзя: это зависимый от
текущей привязки safe no-op с диагностикой, который может начать отправлять
команду после восстановления цели. Здесь требуется явное и стабильное намерение
пользователя.
## Скоуп
- Новая строка «Ничего не делать» в селекторе «Действие по нажатию» для
существующих и новых реальных/виртуальных маркеров.
- Канонический persisted literal `marker.tap_action: "none"`.
- Одинаковый no-op для короткого mouse click, touch tap и keyboard activation
`Enter`/`Space` в View и киоске.
- Сквозное чтение, preview, запись, backend validation, full/space
export/import и downgrade-описание нового literal.
- Паритет переводов EN/RU/DE/FR и обновление пользовательской документации.
- Регресс-проверки остальных действий и default-проекции.
## Не-скоуп
- Изменение долгого нажатия, правого клика или жестов редактора.
- Отдельная настройка hold/right-click action.
- Изменение hit area, hover/focus, внешнего вида, состояния, pulse, Glow, LQI,
value badge, room aggregates или доступности маркера.
- Изменение defaults: свет без явного значения по-прежнему получает Toggle,
остальные устройства — карточку House Plan.
- Изменение семантики `info`, `more-info`, `toggle`, `run` и legacy `cover`.
- Новая HA service/WebSocket-команда, подтверждение или toast для `none`.
- Интерактивность `houseplan-space-card`, которая остаётся отдельной статической
поверхностью по своему существующему контракту.
## Контракт поведения
### 1. Выбор и сохранение
1. `TAP_ACTIONS` содержит канонический `none`; селектор показывает его строкой
**«Ничего не делать»** после существующих четырёх действий.
2. Выбор `none` немедленно меняет draft/effective action диалога. Toggle- и
Run-зависимые поля скрываются по тем же правилам, что для `info` и
`more-info`; блок `controls` остаётся доступен, поскольку он участвует также
в light/presentation graph.
3. Save записывает точный literal `tap_action: "none"`. Повторное открытие
диалога и reload сохраняют выбранную строку.
4. Cancel не меняет конфиг. Простое Open → Save маркера с отсутствующим,
legacy либо неизвестным `tap_action` сохраняет прежний lossless-контракт и
не материализует `none`.
5. `none` не равен `null`, отсутствию или пустой строке. Только явный literal
отключает короткое действие; прежняя light/non-light default-проекция не
меняется.
### 2. Короткое нажатие
1. `projectedTapAction("none", domain)` возвращает `none` для любого domain и
binding kind. Неизвестные токены по-прежнему fail closed в `info`.
2. В View/киоске `_clickDevice()` после разрешения актуального marker по id и
до binding/service веток завершает `none` как quiet no-op.
3. No-op означает одновременно: не открывать карточку House Plan или HA
more-info, не создавать confirmation, не показывать toast, не вызывать HA
service/WebSocket, не запускать press feedback/activity и не менять
локальное/серверное состояние.
4. Click/tap по маркеру остаётся поглощённым маркером и не превращается в клик
по комнате, pan либо другому объекту под ним.
5. `Enter` и `Space` идут тем же `_clickDevice()`-путём, предотвращают
браузерный default как сейчас и также завершаются без побочного действия.
### 3. Независимые жесты и presentation
- Long press 600 ms по-прежнему открывает внутреннюю карточку House Plan.
- Правый клик по-прежнему открывает HA more-info основной сущности либо
существующий fallback для marker без primary.
- В Редакторе устройств короткий клик по маркеру по-прежнему открывает его
настройки; `tap_action` относится только к View/киоску.
- Marker остаётся видимым, focusable и получает прежние hover/focus стили,
tooltip, state, pulse, badge, Glow и LQI. Отдельного disabled-вида нет.
- Preview диалога меняет только effective action; рисунок маркера не меняется.
## UX и i18n
Добавить одинаковый ключ `tap.none` в четыре словаря:
- RU: `Ничего не делать`;
- EN: `Do nothing`;
- DE: `Nichts tun`;
- FR: `Ne rien faire`.
Новая строка находится в существующем native select после `Run`; новая
подсказка, иконка либо секция диалога не нужны. Toggle-hint, target chooser и
confirmation checkbox при `none` не показываются. Live region Toggle не
анонсирует цель.
## Модель данных, миграция и совместимость
### Frontend
- Возвращаемый тип `projectedTapAction()` и все effective-action consumers
расширяются literal `none`.
- `Marker.tap_action` остаётся forward-compatible `string | null`; отдельная
миграция типа или model/store version не требуется.
- `devices` fingerprint уже включает исходный `tap_action`, поэтому смена на
`none` инвалидирует presentation/action snapshot без нового cache key.
### Backend
`MARKER_SCHEMA` принимает `none` рядом с текущими canonical literals. Значение
не несёт target/ref и не требует semantic validator. Cross-language parity
test обязан доказать, что backend принимает каждый literal из frontend
`TAP_ACTIONS`, включая `none`, и продолжает отдельно принимать legacy `cover`.
### Import/export и downgrade
- Full и space export/import сохраняют `none` штатным копированием marker;
remap ссылок не требуется.
- Виртуализация duplicate marker при space import продолжает удалять
`tap_action` вместе с HA-dependent marker fields по существующему правилу.
- Старый frontend прочитает неизвестный ему `none` как `info`; старый backend
отклонит попытку записать изменённый marker с новым literal. Это известная
граница downgrade для нового enum value, а не основание маскировать `none`
под отсутствующее значение.
- Новый frontend/backend не переписывают старые `cover`, absent, `null` или
неизвестные untouched значения при редактировании другого поля.
## Затронутые файлы и модули
- `src/logic.ts`, `src/device-toggle.ts`, `src/houseplan-card.ts`.
- `src/houseplan-editor-runtime.ts`; `src/types.ts` только если потребуется
уточнить локальный effective-action type без сужения forward compatibility.
- `src/i18n/{en,ru,de,fr}.json`.
- `custom_components/houseplan/validation.py`.
- `test/device-toggle.test.mjs`, action/click contract tests,
`tests_backend/test_validation.py` и целевой browser smoke диалога/маркера.
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`,
`docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md`, при необходимости
`docs/TESTING.md`, оба changelog.
## Критерии приёмки
- **AC1 — selector и persisted value (unit + smoke).** Селектор содержит пять
canonical options с `none` последним; выбор сохраняет
`tap_action: "none"`, reopen/reload восстанавливает его, Cancel не пишет.
- **AC2 — quiet no-op (unit + smoke).** Mouse click, touch tap, `Enter` и
`Space` по marker с `none` не открывают поверхности, не вызывают service/WS,
toast, confirmation, press animation или activity stamp.
- **AC3 — независимые жесты (smoke).** Long press всё ещё открывает карточку
House Plan, right click — HA more-info, а клик по тому же marker в Редакторе
устройств — настройки marker.
- **AC4 — presentation parity (unit, DOM assertion).** До и после выбора
совпадают DOM/classes/face/state/pulse/badge/LQI/Glow/tooltip/hit area и
hover/focus; новый disabled-стиль не появляется.
- **AC5 — defaults и legacy (unit).** Absent/`null`/`""` оставляют Toggle для
primary light и Info для остальных; `cover` проецируется в Toggle; неизвестный
token проецируется в Info; untouched Open → Save сохраняет исходный literal.
- **AC6 — соседние действия (unit + smoke).** `info`, `more-info`, `toggle` и
`run` выполняют прежние ветки, а Toggle/Run fields и confirmation видны только
при соответствующих effective actions.
- **AC7 — backend и transfer (backend).** Backend принимает `none` и все
`TAP_ACTIONS`, отклоняет произвольный новый изменённый token; full/space
transfer сохраняет `none`, duplicate virtualization удаляет его как раньше.
- **AC8 — i18n и docs (unit + docs gate).** Четыре словаря имеют паритет,
обе версии руководства описывают `none` и неизменные long/right gestures,
compatibility/architecture фиксируют literal и downgrade.
- **AC9 — гейты и бюджет (commands).** В цикле реализации зелёные
`npx tsc --noEmit`, `npm test`, `npm run build` и целевой backend pytest;
`no-new-any`, i18n/docs checks и browser smoke проходят на обязательном
предрелизном прогоне. Существенного роста initial/editor bundle нет.
## План автотестов
- Расширить таблицу `projectedTapAction`: explicit `none` для light/switch и
неизменные absent/legacy/unknown варианты.
- Добавить целевой тест click dispatcher с шпионами на `_infoCard`,
`_openMoreInfo`, `callService`, `callWS`, `_tapConfirm`, toast,
`_startDevicePressFeedback` и `_stampActivity`.
- Проверить keyboard reuse того же dispatcher и сохранение `stopPropagation` /
`preventDefault` на соответствующих путях.
- Проверить editor draft/save/reopen/Cancel и отсутствие Toggle/Run dependent
controls для `none`; отдельно — lossless untouched legacy/unknown action.
- Расширить cross-language backend test `TAP_ACTIONS` и marker validation.
- Добавить import/export fixtures для full, space и duplicate virtualization.
- Целевой Playwright smoke: выбрать `none`, Save, reopen, reload, выполнить
click/tap/keyboard no-op, long press и contextmenu; убедиться, что marker
presentation не изменился.
Мутанты: не добавить `none` в backend → AC7; спроецировать `none` в `info` →
AC1/AC2; поставить no-op после fallback-карточки → AC2; сделать ранний return до
`stopPropagation` → AC2; связать `none` с unavailable Toggle → AC2/AC5; скрыть
marker или изменить face → AC4.
## Риски
- **Смешение `none` с отсутствием значения.** Truthy/falsy-проверка либо
нестрогий fallback может вернуть light-маркеру Toggle или открыть Info вместо
no-op. Снимается явной веткой `none`, расширенным возвращаемым union и
таблицей absent/`null`/пустого/unknown/legacy/`none` в AC2 и AC5.
- **Неполный охват потребителей `tap_action`.** Помимо selector и click path
поле участвует в `devices` fingerprint, virtual-light и cover presentation.
Перед реализацией выполняется полный поиск consumers; код меняется только в
action projection/dispatch, а AC4/AC6 фиксируют неизменность presentation и
специальных Toggle-потребителей.
- **Ранний return в неверной точке.** Если выйти до `stopPropagation`, нажатие
протечёт в комнату/план; если выйти после fallback, откроется карточка.
Dispatcher-тест и соответствующие мутанты фиксируют точное окно между
разрешением актуального marker и capability/info ветками.
- **Mixed-version/downgrade.** Новый frontend с прежним backend не сможет
сохранить `none`; полный откат backend после уже записанных значений сделает
последующую запись такого конфига невалидной. Интеграция поставляет frontend
и backend одним релизом, а безопасный порядок аварийного отката описан ниже и
сохраняет backend read/write allowlist до очистки данных.
- **Пассивное нажатие могут принять за поломку.** Выбор называется буквально
«Ничего не делать», отдельного disabled-вида нет, а long press/right click
остаются документированными. Smoke проверяет, что исчезло только выбранное
короткое действие, а не сам marker или информационные жесты.
## Откат
Feature flag и миграция отсутствуют. Безопасный аварийный откат выполняется в
два этапа:
1. revert-нуть selector/runtime/i18n/docs-часть пользовательского изменения,
но временно **оставить** `"none"` в backend `MARKER_SCHEMA`; reverted
frontend уже проецирует этот неизвестный literal в безопасный `info`, а
существующие конфиги продолжают загружаться и сохраняться;
2. если требуется убрать literal полностью, отдельной проверяемой data-fix
заменить сохранённые `tap_action: "none"` на `"info"`, и только после этого
удалить его из backend allowlist.
Полный слепой `git revert` frontend и backend одним шагом запрещён после выхода
релиза: уже записанный `none` останется в store, старый frontend покажет Info,
но старый backend отклонит следующую запись всего конфига. Потеря данных при
безопасном откате ограничена осознанным пользовательским намерением no-op,
которое становится `info`; геометрия, marker binding, presentation и остальные
поля не меняются. После любого отката повторяются backend validation, config
Open → Save и click smoke на конфиге, содержащем `none`.
## Производительность и безопасность
No-op — одна константная ветка до capability resolution и сетевых операций.
Новых подписок, таймеров, cache entries, DOM-узлов и данных HA нет. Вызовов
service/WS для `none` быть не может; backend принимает только точный allowlisted
literal. Full performance/golden harness не требуется из-за отсутствия
геометрической/визуальной дельты, но штатные предрелизные проверки проекта не
отменяются.
## Release-артефакты
- Пользовательская запись в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том
же `User-Visible: yes` коммите, что реализация.
- Обновлённые EN/RU user guides, compatibility и architecture.
- Новый plan golden не требуется: marker pixels и геометрия не меняются.
Открытое native select не является стабильной кроссплатформенной golden-
поверхностью; наличие/текст option доказывают DOM unit и Playwright smoke.
- Docs screenshot не требуется: существующие изображения не утверждают полный
список tap actions; текстовые разделы являются каноническими.
- Release/performance/security waiver не требуется; перед beta выполняется
общий golden/smoke/performance набор по runbook.
## Принятые предположения
- `none` расположен последним в текущем порядке selector; порядок можно свободно
поменять до реализации без изменения контракта.
- Controls остаются редактируемыми, потому что используются presentation/light
graph независимо от короткого действия.
- Существующая политика соседних полей сохраняется: смена с `run` на любое
другое действие очищает `tap_target` при записи, а общие настройки marker не
сбрасываются только из-за выбора `none`.
- Marker остаётся focusable и визуально интерактивным ради доступных hover,
tooltip, long-press и context-menu путей; отдельный disabled appearance не
вводится.