23 KiB
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и legacycover. - Новая HA service/WebSocket-команда, подтверждение или toast для
none. - Интерактивность
houseplan-space-card, которая остаётся отдельной статической поверхностью по своему существующему контракту.
Контракт поведения
1. Выбор и сохранение
TAP_ACTIONSсодержит каноническийnone; селектор показывает его строкой «Ничего не делать» после существующих четырёх действий.- Выбор
noneнемедленно меняет draft/effective action диалога. Toggle- и Run-зависимые поля скрываются по тем же правилам, что дляinfoиmore-info; блокcontrolsостаётся доступен, поскольку он участвует также в light/presentation graph. - Save записывает точный literal
tap_action: "none". Повторное открытие диалога и reload сохраняют выбранную строку. - Cancel не меняет конфиг. Простое Open → Save маркера с отсутствующим,
legacy либо неизвестным
tap_actionсохраняет прежний lossless-контракт и не материализуетnone. noneне равенnull, отсутствию или пустой строке. Только явный literal отключает короткое действие; прежняя light/non-light default-проекция не меняется.
2. Короткое нажатие
projectedTapAction("none", domain)возвращаетnoneдля любого domain и binding kind. Неизвестные токены по-прежнему fail closed вinfo.- В View/киоске
_clickDevice()после разрешения актуального marker по id и до binding/service веток завершаетnoneкак quiet no-op. - No-op означает одновременно: не открывать карточку House Plan или HA more-info, не создавать confirmation, не показывать toast, не вызывать HA service/WebSocket, не запускать press feedback/activity и не менять локальное/серверное состояние.
- Click/tap по маркеру остаётся поглощённым маркером и не превращается в клик по комнате, pan либо другому объекту под ним.
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 расширяются literalnone. Marker.tap_actionостаётся forward-compatiblestring | null; отдельная миграция типа или model/store version не требуется.devicesfingerprint уже включает исходный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: explicitnoneдля 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 поле участвует вdevicesfingerprint, 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 и миграция отсутствуют. Безопасный аварийный откат выполняется в два этапа:
- revert-нуть selector/runtime/i18n/docs-часть пользовательского изменения,
но временно оставить
"none"в backendMARKER_SCHEMA; reverted frontend уже проецирует этот неизвестный literal в безопасныйinfo, а существующие конфиги продолжают загружаться и сохраняться; - если требуется убрать 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 не вводится.