diff --git a/docs/specs/381-no-op-tap-action.md b/docs/specs/381-no-op-tap-action.md new file mode 100644 index 00000000..d1b3f82f --- /dev/null +++ b/docs/specs/381-no-op-tap-action.md @@ -0,0 +1,255 @@ +# Issue #381 — действие по нажатию «Ничего не делать» + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/381 +- **Статус документа:** первая редакция, готова к ревью +- **Приоритет / тип:** P2 · feature / polish +- **Область:** marker config, Редактор устройств, View/киоск, frontend action + projection, backend validation, i18n, документация и QA +- **Связи:** #94 (универсальный Toggle), #178 (точная Toggle-сущность) +- **Ревизия:** 1 (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 + visual 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. + +## Производительность и безопасность + +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 не + вводится. diff --git a/docs/specs/README.md b/docs/specs/README.md index 7c490bb0..eec3e0fa 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -1,6 +1,6 @@ # Спецификации задач -Актуально на 2026-08-29. +Актуально на 2026-08-30. GitHub Issues и GitHub Projects (v2) остаются единственным каноническим backlog проекта. Этот каталог содержит развёрнутые ТЗ: каждое ТЗ ссылается на issue, а issue — на соответствующий файл. Статус, приоритет и факт завершения меняются только в GitHub. @@ -114,6 +114,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#84](https://github.com/Matysh/houseplan-card/issues/84) Источник «Всегда» без сущности HA + [#88](https://github.com/Matysh/houseplan-card/issues/88) ведущая сущность | [084-passive-forced-light-sources.md](084-passive-forced-light-sources.md) | | [#90](https://github.com/Matysh/houseplan-card/issues/90) Управляемый бейдж со значением | [090-device-value-badge.md](090-device-value-badge.md) | | [#378](https://github.com/Matysh/houseplan-card/issues/378) Выбираемый источник для режима «Значение + состояние» | [378-value-face-source.md](378-value-face-source.md) | +| [#381](https://github.com/Matysh/houseplan-card/issues/381) Действие по нажатию «Ничего не делать» | [381-no-op-tap-action.md](381-no-op-tap-action.md) | | [#94](https://github.com/Matysh/houseplan-card/issues/94) Универсальное действие «Переключить состояние» | [094-universal-state-toggle.md](094-universal-state-toggle.md) | | [#101](https://github.com/Matysh/houseplan-card/issues/101) Плавный переход View ↔ редакторы | [101-view-editor-transition.md](101-view-editor-transition.md) | | [#107](https://github.com/Matysh/houseplan-card/issues/107) Переключение виртуального источника света «Всегда» | [107-virtual-light-toggle.md](107-virtual-light-toggle.md) |