# 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 не вводится.