Compare commits

..
Author SHA1 Message Date
claude[bot] a28a4b7065 docs: review document for #149
Validate / provenance (push) Successful in 1m3s
Validate / process-gate (push) Successful in 38s
Validate / changes (push) Successful in 27s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / smoke (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
Issue: #149
User-Visible: no
2026-08-18 19:31:47 +00:00
Sergey Matyunin 2d5f0a5f0f docs(spec): define touch view settings affordance
Issue: #149
User-Visible: no
2026-08-15 10:39:05 +03:00
4 changed files with 616 additions and 479 deletions
+264
View File
@@ -0,0 +1,264 @@
# Ревью ТЗ — issue #149, цикл r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/149
- **ТЗ:** `docs/specs/149-touch-view-settings-affordance.md`, коммит `2d5f0a5f0f66faf1614a9f1f367ce108011fcfbe`
- **Этап:** spec (PROCESS.md §2.4)
- **Трек:** обычный (не `small`/`trivial` — сам issue называет причину: новый
видимый элемент, таймер, снятие существующего жеста, попадание в геометрию
контура, затронуты все три персоны); лимит цикла — 4
- **Вердикт:** красный · цикл r1/4 · High: 1 · Medium: 0
## Скоуп ревью
Прочитаны: `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (§1, §2.2–2.5, §3, §4,
§7.1–7.2, §12), тело issue #149 и все три комментария владельца (аналитика с
Q1–Q3 и defaults, решение владельца по Q1–Q3, объявление о готовом ТЗ), весь
текст `docs/specs/149-touch-view-settings-affordance.md`, `docs/UX-MODES.md`
(принцип режимов, политика ввода, kiosk-раздел), `docs/TOUCH-SUPPORT.md`
(контракт, safety floor, deliberate degradation), `docs/CANVAS.md` (§1–4,
модель content frame — чтобы не путать её с новым «physical footprint»),
`docs/WALL-THICKNESS.md` (§1–4, модель стен/partition/column, кэширование
структурного прохода), `docs/USER-GUIDE.ru.md` (раздел 6 «Навигация, масштаб и
жесты», раздел 17 «Киоск-режим», полнотекстовый поиск терминов «настройки
вида» и «киоск»), `docs/CONFIG-COMPATIBILITY.md` (чтобы подтвердить, что задача
действительно не задевает реестр совместимости server config).
Дополнительно, поскольку ТЗ описывает удаляемое/переносимое поведение как
факт о текущем продукте, а не как предположение — прочитан сам код:
`src/houseplan-card.ts` (`_stagePointerDown` ок. строк 5082–5104,
`_kioskScale`/`_kioskDialog`/`LS_KIOSK` ок. строк 1551–1553, 2313–2318,
14244–14277, применение множителя в devlayer — строка 14725,
`_interruptViewGesture` — строка 4300), `src/i18n/ru.json`/`en.json` (ключи
`kiosk.*`), `demo/smoke_kiosk.mjs`, `docs/CHANGELOG.md` (запись v1.41.0, где
эта функция впервые появилась) и `docs/TESTING.md` (пункт «Kiosk mode»,
строки 768–774).
## Как проверялось
1. Сверил формальные обязательные разделы ТЗ (PROCESS.md §7.1) с текстом
`docs/specs/149-touch-view-settings-affordance.md`: сценарий и персона/
поверхность/момент — §1; что человек увидит до/после — §1; скоуп и
не-скоуп — §4/§5; контракт поведения — §6–9; UX — §8/§10; модель данных и
миграция — §11; i18n — §10/§14; AC1…AC12 — §12; план автотестов — §13;
риски — §15; откат — §15; release-артефакты — §14. Раздела с буквальным
заголовком «Проблема» нет, но его содержание (почему долгий тап — плохой
affordance) присутствует в issue и подразумевается мотивацией §1; отмечаю
как Low ниже.
2. Прогнал через код каждое фактическое утверждение ТЗ о «существующем»
поведении (§1, §4 п.4, §9, §11), а не поверил формулировке — именно это
PROCESS.md называет «догадкой, выданной за решение», и именно на это
ревьюер обязан ловить автора.
3. Проверил все три owner-defaults (Q1–Q3 из комментария
`#issuecomment-5298450448`, подтверждённых `#issuecomment-5301106992`) на
соответствие тексту ТЗ §2 — совпадают буквально.
4. Проверил геометрические термины ТЗ (§6: «physical footprint», «unbounded
exterior», holes/courtyard, detached porch/terrace) на отсутствие
конфликта с уже канонической моделью `CANVAS.md` (content frame — другая
сущность, используется для камеры, не для hit-теста фона) и
`WALL-THICKNESS.md` (thick walls, partitions, columns, кэш по structural
fingerprint) — конфликтов не нашёл, переиспользование корректно оговорено
как предположение №1 в §16.
5. Проверил i18n-термин «Настройки вида» на предмет «терминология изобретена,
а не взята из USER-GUIDE» — в `docs/USER-GUIDE.ru.md` этой фразы сейчас
нет вообще (полнотекстовый поиск — 0 совпадений); фраза происходит из
заголовка/тела issue (то есть от владельца), не придумана автором ТЗ, и
ТЗ уже включает обновление USER-GUIDE в release-артефакты (§14) — это
штатный путь введения нового термина, не нарушение.
6. Проверил каждый AC (§12, 12 пунктов) на однозначность и на то, назван ли
способ доказательства в §13 — не построчным сопоставлением заголовков (в
ТЗ они не пронумерованы 1:1 с AC), а по содержательному пересечению
формулировок.
## Находки
### High-1 — §11 утверждает как факт то, что код опровергает; §4 не включает
необходимое для этого утверждения изменение
**Что не так.** §11 «Модель данных и compatibility» пишет: «kiosk и normal
View используют один per-screen value, **как и до изменения**» — то есть
заявляет, что до этой задачи обычный View и kiosk уже одинаково применяют
`_kioskScale` (масштаб иконок/текста) к рендеру. Это не так.
**Воспроизведение по коду (проверено чтением, не исполнением):**
- `src/houseplan-card.ts:5088` — весь блок, открывающий диалог размеров по
долгому тапу, обёрнут в `if (this._kiosk) { … }`. В обычном View
(`kiosk: false`) этот код не выполняется вовсе — то есть сегодня в обычном
View нет *никакого* способа, ни скрытого, ни явного, открыть этот диалог.
- `src/houseplan-card.ts:14725` — множитель фактически применяется тоже
только в kiosk: `` this._kiosk ? this._kioskScale.icon : 1 `` и то же для
`--rl-font`. Значение читается из `localStorage` (`LS_KIOSK`,
строки 2313–2318) независимо от режима, но **применяется к рендеру** icon/
font только когда `this._kiosk === true`.
- `docs/USER-GUIDE.ru.md:185–193` — таблица жестов документирует «Сброс
киоск-масштаба» и «Локальный размер киоска» строго в столбце «Сенсорный
экран» **киоска**, без единой строки для обычного View; это не пробел в
документации, это точное описание текущего продукта.
- `docs/CHANGELOG.md:2826–2836` (v1.41.0) — функция изначально задумана и
описана как kiosk-only: «every tablet/TV tunes itself once». Ограничение
не случайно, это осознанное решение выпуска 1.41.0, а не забытый кусок кода.
**Почему это блокирует, а не техническая деталь на усмотрение автора.**
Вопрос «увидит ли пользователь эффект от сдвига слайдера в новом диалоге,
открытом кнопкой в обычном View» — продуктовый и наблюдаемый, а не
внутренняя реализация. Как написано сейчас, реализация по ТЗ откроет в
обычном View **тот же диалог с теми же слайдерами**, но применение
`this._kiosk ? … : 1` в devlayer никто не просит поменять — §4 «Скоуп» не
называет это пунктом работы, §16 «Принятые технические предположения» не
называет это допущением, а §11 прямо утверждает обратное факту. Результат:
кнопка появится у домочадца и гостя на телефоне/планшете (как и требует
owner-решение «доступны всем touch-поверхностям, отдельного поведения для
киоска не делаем»), но движение слайдера «Размер значков устройств» ничего
не изменит на экране — рабочий контрол, который визуально не работает. Это
прямое попадание в `docs/TOUCH-SUPPORT.md`: «A touch-only failure in View is
a product defect, not an accepted limitation of the editor policy» — только
здесь дефект дня выпуска, а не более позднего открытия.
Это также ровно тот класс дефекта, который PROCESS.md называет «худшим
видом»: утверждение о поведении («используют один per-screen value, как и до
изменения»), которого не подтверждает ни один документ и которое не помечено
как предположение — оно читается как решённый факт и поэтому проходит ревью
на автомате, если его не сверить с кодом.
**Не эскалируется владельцу как новый вопрос.** Сам факт нужности такого
изменения не требует решения владельца: намерение уже зафиксировано его же
формулировкой «Настройки вида доступны всем touch-поверхностям… отдельного
поведения для киоска не делаем» (issue, «Решения владельца»,
подтверждено `#issuecomment-5298450448`). Контрол, открытый на всех
поверхностях, но работающий только на части из них, — это не то, что
владелец согласовывал; значит нужно не спрашивать, а **дописать ТЗ**: явно
включить в §4 «Скоуп» снятие/обобщение условия `this._kiosk ?` в применении
множителя к рендеру (или эквивалентное решение), завести под это отдельный
AC («изменение слайдера в обычном View видимо меняет размер иконок/текста
так же, как в kiosk») и соответствующий unit/smoke пункт в §13, и поправить
§11, чтобы он описывал целевое, а не мнимое текущее поведение.
**Серьёзность.** High — блокирует переход в `S5-ready`. Без исправления
задача либо тихо доставит нерабочий контрол в двух из трёх персон (что
`docs/SCOPE.md`/`TOUCH-SUPPORT.md` не допускают), либо реализация по ходу
дела сама «дорешит» продуктовый вопрос без записи, что запрещено §7.1
(«размытое место не додумывается, а выносится либо явно фиксируется»).
## Что проверено и корректно
- **Owner-решения Q1–Q3 перенесены буквально.** §2 ТЗ воспроизводит все три
default-а (`touch/coarse only`, 5-секундный таймер с перезапуском и
немедленным скрытием на pan/zoom/смену пространства, «вне плана» =
unbounded exterior с исключением detached porch/terrace и holes) без
искажений и без добавления новых решений от себя.
- **Геометрический контракт (§6) не путает две разные модели.** «Physical
footprint» (архитектура: floor+walls+partitions+columns, без decor/devices/
Glow/sun) корректно отличается от `contentFrame` из `CANVAS.md` (камера/
fit, включает decor и devices) — общий пул терминов не смешан, никакой
скрытой ре-дефиниции существующего понятия нет.
- **Производительность (§6.3, AC12).** Требование «топология считается
только при structural fingerprint change, не на pointermove/HA tick»
прямо аналогично уже работающей модели `WALL-THICKNESS.md` §2/§3 («one
cached structural pass in flat renderers; live HA state ticks do not
repeat the boolean topology») — это перенос уже принятого паттерна, а не
новая непроверенная идея.
- **Владение жестом (§7) корректно разграничивает hit-test фона и клики по
интерактивным целям**, явно перечисляет device/room/opening/vacuum/header/
dialog как приоритетные над новым «чистым тапом», и явно требует, чтобы
`preventDefault()` не расширялся за пределы нового чистого тапа — это
прямое соответствие safety floor `TOUCH-SUPPORT.md` (`pinch`/`pointercancel`/
второй палец не должны читаться как клик).
- **Права (§9).** Явно и неоднократно (§9, AC3, AC11) отделяет новый affordance
от административного `_settingsDialog`/«Общих настроек» и от `_canEdit` —
соответствует SCOPE.md (lock invariant в духе «намеренно, не по случайности»
распространён здесь на права: контрол для non-admin не получает доступа к
admin-поверхности).
- **Accessibility (§10).** Требование «focusable button, не SVG-only»,
локализованные aria-label, focus trap диалога, приостановка auto-hide при
keyboard focus — конкретно и проверяемо, ссылается на существующий dialog
contract, а не изобретает новый.
- **Совместимость (§11), помимо разобранного выше пункта.** Отсутствие
миграции, отсутствие новых серверных полей, отсутствие новых полей
`localStorage` подтверждено — задача действительно не задевает
`docs/CONFIG-COMPATIBILITY.md` (реестр — про server config/backend
validation, сюда не относится), и в «Связано» этот документ обоснованно не
включён.
- **Термин «Настройки вида» не изобретён автором ТЗ** — он происходит от
формулировки владельца в issue и вводится в канон через release-артефакты
§14 (обновление `docs/USER-GUIDE.ru.md`), что штатный путь.
- **Не-скоуп (§5) конкретен и вычёркивает реальные соседние риски**
(desktop mouse/hover, «Общие настройки», миграция диапазонов масштаба,
постоянная кнопка в header, показ по внутренним поверхностям) — не общая
фраза «остальное не трогаем».
- **Откат (§15)** описан предметно: удаление временной кнопки, возврат кода
без миграции, совместимость `localStorage`/server config — проверяем без
code review.
- **Трек и лимит цикла** выбраны верно: задача явно не `small`/`trivial` по
собственному разделу «Влияние» issue, лимит цикла — 4, документ лежит по
канону `docs/specs/149-touch-view-settings-affordance.md`.
## Low — не блокируют, зафиксированы с решением ревьюера
1. **Нет раздела с буквальным заголовком «Проблема».** Содержание есть
(в issue и в мотивации §1), но в самом файле ТЗ не выделено отдельным
заголовком, как того просит PROCESS.md §7.1. Снимается без правки: смысл
присутствует и не расходится с issue; при следующей правке (см. High-1)
стоит добавить подзаголовок «Проблема» из issue «Почему это стоит сделать»
заодно, но отдельного цикла ревью на это не требуется.
2. **AC8, AC10, AC12 не имеют явно названного способа доказательства в §13.**
§13 сгруппирован по категориям (Unit / Touch smoke / Golden), а не 1:1 по
номеру AC, и для AC8 («desktop mouse/hover не меняется, mouse click не
показывает кнопку»), AC10 (accessibility/focus) и AC12 (geometry cache)
не нашлось однозначно соответствующего пункта ни в одном из трёх списков.
Не блокирует понимание контракта, но при возврате на исправление High-1
стоит добавить: unit-пункт на `pointerType !== 'touch'/coarse'` → кнопка
не показывается; smoke/unit-пункт на focus trap и aria-label; unit/perf-
пункт, считающий вызовы построения footprint на серию тапов без изменения
геометрии. Снимается автором в той же правке, без нового issue — это
дополнение к тесту уже принятого AC, а не отдельная находка о продукте.
3. **Название кнопки и название диалога расходятся терминологически.**
Предложенный aria-label/title кнопки — «Настройки вида» / «View settings»
(§10), а существующий заголовок диалога в `ru.json`/`en.json`
(`kiosk.title`) — «Размеры на этом экране» / текущий английский эквивалент.
Тап по кнопке «Настройки вида» открывает диалог, который сам себя называет
иначе. Не мешает работе функции и не противоречит ни одному документу —
отмечаю, чтобы при реализации выбрали одно из двух: либо оставить как
мелкое, но заметное расхождение вида, либо привести название диалога и
aria-label к одному термину в release-артефактах (§14 это разрешает).
## Чего не проверял
- Реализацию — кода `src/**` под это issue ещё нет (ветка
`issue/149-touch-view-settings-affordance` на момент ревью содержит только
файл ТЗ, `git log --oneline` подтверждает единственный коммит `2d5f0a5`).
Проверка кода на этом этапе не требуется PROCESS.md §2.4.
- Golden-эталоны и живые performance-профили — не существуют до реализации;
§13 описывает план, не результат.
- Полный текст `docs/SUN.md`/`docs/LIGHT.md` — не читал построчно: задача не
трогает освещение/солнце (§5 явно перечисляет их эффекты как то, что не
расширяет physical footprint, но не меняет), релевантно только фактом, что
их эффекты не входят в hit-test, что уже подтверждено §6.1 текста ТЗ, и
этого достаточно для этого ревью.
- `docs/specs/README.md` — не проверял, зарегистрирована ли запись на этот
документ там; при пересмотре в r2 стоит свериться, но это не продуктовая
находка.
- Существующие браузерные smoke-файлы (`demo/smoke_kiosk*.mjs` и др.) прочитаны
только частично, ровно в объёме, нужном чтобы подтвердить/опровергнуть
утверждения ТЗ о текущем поведении (см. «Как проверялось» и High-1);
полный аудит покрытия smoke-suite не проводился — на этапе ТЗ без
реализации это не даёт дополнительной информации.
## Вывод
ТЗ методологически сильное: geometry-контракт корректно отделён от camera-
модели `CANVAS.md`, owner-решения перенесены буквально, safety floor и права
разобраны конкретно, откат и не-скоуп предметны. Но в основании ТЗ лежит
фактическая ошибка о текущем продукте (§11: «kiosk и normal View используют
один per-screen value, как и до изменения»), которую код прямо опровергает:
сегодня масштаб иконок/текста применяется к рендеру только в kiosk, а в
обычном View — никогда, вне зависимости от значения в `localStorage`. Раз
owner-решение требует единого affordance на всех touch-поверхностях, эта
задача не «делает видимым существующий скрытый вход», а частично **создаёт
новую функцию для обычного View** — и эта часть работы не названа ни в
скоупе, ни в AC, ни как принятое допущение. Это ровно тот guess-as-fact,
который PROCESS.md прямо называет худшим видом дефекта, потому что он
выглядит решением и проходит ревью, если его не сверить с исходником.
Возврат в `S3-spec`. Исправление: явно добавить в §4/§12/§13 работу по
применению множителя иконок/текста в обычном View (не только kiosk),
поправить факт в §11, и по возможности закрыть три Low-пункта той же правкой.
@@ -1,477 +0,0 @@
# #109 — отдельные markers для каналов многоканального HA-устройства
- Issue: [#109](https://github.com/Matysh/houseplan-card/issues/109)
- Приоритет: P2
- Тип: bug
- Ветка: `issue/109-multichannel-entity-binding`
- Статус документа: полное ТЗ подготовлено; issue остаётся в `S3-spec`, review не запущено по прямому указанию владельца
- Основание: отчёт пользователя и принятые владельцем defaults от 2026-08-15
## 1. Пользовательская проблема и результат
Home Assistant может представить один физический многоканальный выключатель как
одно device с несколькими независимыми entities. В исходном случае два канала
света управляются через MQTT: в House Plan устройство находится, но отдельный
`mqtt light` второго канала не находится даже в режиме выбора сущностей, хотя
соседние `mqtt sensor` того же устройства видны. В результате на план можно
поставить только один общий marker и управлять только первым каналом.
После #109 администратор дома включает «Показывать сущности», находит каждый
активный канал по понятному имени или точному `entity_id` и создаёт для него
отдельный marker. Каждый marker показывает состояние и выполняет действие только
для своей exact entity. House Plan не создаёт несколько markers автоматически и
не меняет семантику общего `device:*` marker.
## 2. Персона, поверхность и before/after
- **Основная персона:** Home admin с многоканальным реле/выключателем в HA.
- **Настройка:** Device editor в desktop browser; touch остаётся best effort по
общему контракту редактора.
- **Использование результата:** Full View, Static card и существующие быстрые
действия exact entity marker.
- **До:** один HA device фактически схлопывает два независимых канала, а второй
канал невозможно гарантированно найти и разместить.
- **После:** два канала одного HA device независимо находятся, сохраняются и
управляются как два exact entity markers.
## 3. Итог аналитики
### 3.1 Ценность, сложность и риск
- пользовательская ценность: 7/10 — без исправления часть реального освещения
нельзя разместить и управлять с плана;
- ценность для продукта: 5/10 — устраняется разрыв между entity-моделью HA и
уже существующей exact-binding моделью House Plan;
- сложность: 4/10 — формат данных и runtime exact binding уже существуют;
- риск: 5/10 — discovery использует registry, live states, tombstones,
дедупликацию и ограничение выдачи, поэтому локальный фильтр без единого
контракта может открыть disabled/удалённые ссылки или снова скрыть sibling.
Классификация остаётся **P2 bug, обычный процесс**. Задача входит в J1, J3 и J6
`docs/SCOPE.md`: достоверный live-план, безопасное действие с плана и сохранение
актуальности плана при развитии дома.
### 3.2 Подтверждённая техническая база
На момент подготовки ТЗ:
- marker уже хранит точную привязку в виде `entity:<entity_id>`;
- exact entity marker уже имеет собственную identity, state и action target;
- Device editor показывает device candidates всегда, а individual entities —
после флага `showEntities`;
- `_bindingCandidates()` отдельно проходит активную registry-проекцию,
исключает уже занятые exact bindings и фильтрует результат перед лимитом 200;
- `resolveHaBindingStatus()` уже является каноническим источником статуса
`active`, `ha_disabled`, `orphaned` и `unverified`;
- `activeRegistryHass()` намеренно сохраняет live states без registry row, но
текущий individual-entity candidate loop проходит только `h.entities`, из-за
чего критерии «entity активна» и «entity можно выбрать» расходятся;
- текущий smoke проверяет включение «Показывать сущности» и выбор одной entity,
но не два sibling channels одного device, точный поиск, лимит 200 и
независимое повторное добавление.
### 3.3 Связанные задачи и границы
- #29 — общий lifecycle/inbox устройств; не дубликат.
- #88 — выбор ведущей сущности **внутри одного** forced-light marker; не меняет
создание отдельных markers и не дублирует #109.
- #117 — parity registry-less YAML entity у архитектурного проёма; остаётся
отдельной задачей, потому что #109 не меняет `opening.contact`/`opening.lock`
и рендер проёмов.
## 4. Нормативные продуктовые решения
1. Один независимо управляемый канал = один явно созданный пользователем exact
entity marker.
2. Автоматически разворачивать один HA device в несколько markers запрещено.
3. Наличие `device:*` marker не занимает и не скрывает его отдельные
`entity:*` channels; занята только совпадающая exact binding.
4. Две entities одного device не дедуплицируются между собой по parent device,
имени, area или domain.
5. «Показывать сущности» остаётся явным opt-in: при выключенном флаге обычные
child entities устройства не заполняют список.
6. После включения флага каждая активная exact entity должна быть находима по
полному `entity_id`, friendly/registry name, domain и имени parent device.
7. Поиск применяется до лимита выдачи. Точное совпадение за пределами первых
200 нефильтрованных строк всё равно должно появиться в результате.
8. HA-hidden, но enabled entity не равна HA-disabled. В advanced-режиме
«Показывать сущности» она остаётся доступна через поиск.
9. Явно disabled entity, entity disabled вместе с parent device, orphaned либо
unverified binding не предлагается для нового marker.
10. Exact live entity без registry row может быть кандидатом, если канонический
binding-status resolver классифицирует её как `active`; отсутствие registry
metadata не должно само по себе скрывать рабочую сущность.
11. Marker с `removed: true` сохраняет существующий re-add contract: его exact
binding можно выбрать снова, и новая запись заменяет tombstone.
12. Уже размещённая exact entity не предлагается повторно, кроме текущей
привязки редактируемого marker.
13. Если требуемая entity не может быть подтверждена как active, UI не создаёт
полурабочую ссылку и не подменяет её первым sibling channel.
## 5. Scope
### 5.1 Discovery и поиск
- единый чистый resolver кандидатов для device/entity binding picker;
- union подтверждённых active registry entities и допустимых active live-only
exact entities без потери sibling channels;
- поиск по name, `entity_id`, domain и parent device name;
- детерминированная сортировка и limit-after-filter;
- сохранение существующих helper/group candidates и device candidates;
- согласованная обработка taken, removed, disabled и limited-registry states.
### 5.2 Сохранение и runtime
- создание отдельных markers с bindings `entity:<channel-a>` и
`entity:<channel-b>`;
- независимая marker identity и layout position;
- существующие exact-entity presentation/state/action paths без выбора первого
sibling устройства;
- отсутствие автоматической мутации других markers и parent device marker.
### 5.3 Проверки и документация
- unit matrix для candidate resolver;
- browser smoke для двух каналов одного устройства;
- regression existing device/helper/group/tombstone behavior;
- RU user guide: как разместить каналы многоканального устройства отдельно;
- архитектурная документация единого active-candidate contract.
## 6. Non-scope
- автоматическое создание markers для всех каналов устройства;
- bulk placement, grouping либо визуальная связь sibling markers;
- изменение HA/MQTT integration, Entity Registry или device/entity model HA;
- выбор ведущей light entity внутри одного marker — это #88;
- изменение агрегации состояния и service targets общего `device:*` marker;
- изменение `opening.contact`, `opening.lock` или registry-less рендера проёмов
— это #117;
- повторное использование одной exact entity двумя markers;
- новый вид marker, новые иконки, badges, animations или настройки действий;
- изменение публичности скрытого изометрического режима;
- автоматическая миграция существующих device markers в entity markers.
## 7. Контракт кандидатов
### 7.1 Вход и выход
Candidate resolver получает неизменяемый snapshot текущего HA frame и контекст
диалога:
```ts
interface BindingCandidateContext {
hass: HomeAssistant;
registrySnapshot: HaRegistrySnapshot;
markers: MarkerCfg[];
devices: DevItem[];
editingMarkerId?: string;
editingBinding?: string;
showEntities: boolean;
filter: string;
limit: number; // UI default: 200
}
interface BindingCandidate {
value: `device:${string}` | `entity:${string}`;
label: string;
sub: string;
}
```
Конкретные имена types/functions не нормативны. Нормативны один общий resolver,
чисто тестируемые решения и отсутствие второго расходящегося entity-фильтра в
render method.
### 7.2 Eligibility exact entity
Entity допустима как новый candidate, когда одновременно выполнено:
1. `resolveHaBindingStatus(hass, entity:<id>, snapshot).kind === 'active'`;
2. exact binding не занята другим не-removed marker;
3. это не удалённая HA entity и не tombstone другого binding;
4. включён `showEntities`, либо entity уже относится к существующей категории
standalone group/helper, которая показывается по старому контракту;
5. строка проходит непустой пользовательский фильтр, если он задан.
Registry metadata используется для name, parent device и platform. Если строки
нет, candidate строится из live state: label = `friendly_name || entity_id`,
domain берётся из `entity_id`, parent device name отсутствует. Это не означает,
что любой текстовый id принимается: требуется положительный active status.
HA `hidden_by`/эквивалентный presentation flag не является `disabled_by` и не
исключает active entity из advanced exact search. В #109 отдельный warning/badge
для hidden-by-HA не вводится.
### 7.3 Taken и sibling semantics
- `entity:light.bath` занимает только `entity:light.bath`;
- он не занимает `entity:light.toilet`, даже если обе registry rows имеют один
`device_id`;
- `device:dual_relay` не занимает ни одну child `entity:*` binding;
- существующие name/area dedup rules применяются только к device candidates и
не применяются к entity candidates;
- tombstone `removed: true` не считается занятой binding и остаётся доступным
для явного восстановления;
- при редактировании marker его текущая binding остаётся видимой/выбранной, но
не разрешает создать отдельный дубль.
### 7.4 Label, secondary text, search и sort
Для registry entity:
- `label = registry name || live friendly_name || entity_id`;
- `sub` содержит domain, локализованное «Сущность», parent device name при его
наличии и точный `entity_id`;
- две одинаковые labels различимы по `entity_id`.
Нормализация поиска: Unicode lowercase + trim; поиск — substring по объединению
label, secondary text и exact value. Дополнительная транслитерация/fuzzy search
не требуется.
Порядок вычисления:
1. построить eligible candidates;
2. применить filter;
3. отсортировать по localized label, затем по exact value как tie-breaker;
4. применить limit 200.
## 8. UX-поток
1. Пользователь открывает Device editor и «Добавить».
2. Включает существующий флаг «Показывать сущности».
3. Вводит `light.bath`, `light`, «Ванная» либо имя parent device.
4. Выбирает первый канал, сохраняет marker и размещает его.
5. Снова открывает «Добавить»: первый exact channel отсутствует как занятый,
второй sibling остаётся в выдаче.
6. Выбирает второй канал и сохраняет второй marker.
7. В View действие по первому marker адресует только первый entity id, действие
по второму — только второй.
Если channel исчез/disabled до Save, обычная повторная validation не должна
сохранять его как active candidate. Тихо выбирать parent device или sibling
запрещено. Отдельный новый modal/error text не требуется, если существующий
refresh/validation flow честно снимает candidate и не делает partial save.
## 9. Данные, миграция и совместимость
Новый persisted field и migration step не нужны. Канонические записи уже имеют
достаточную форму:
```json
{
"markers": [
{ "id": "entity_light_bath", "binding": "entity:light.bath" },
{ "id": "entity_light_toilet", "binding": "entity:light.toilet" }
]
}
```
Требования:
- существующие `device:*`, `entity:*`, `virtual` markers читаются без rewrite;
- import/export сохраняет обе exact bindings буквально;
- layout остаётся keyed существующей marker identity;
- сохранение второго sibling marker не переписывает первый и parent marker;
- rollback к версии до #109 остаётся data-safe: старый runtime уже понимает
exact entity markers, хотя старый Add picker может не найти их заново;
- неизвестные sibling config fields и порядок незатронутых markers сохраняются
по общему compatibility contract.
## 10. State, presentation и действия
#109 не создаёт новый resolver состояния или действий. После выбора используются
существующие exact-entity authorities:
- state/presentation получают только bound entity;
- universal toggle проверяет domain и безопасность этой entity;
- service target не расширяется до всех entities parent device;
- unavailable/unknown остаются по существующему fail-safe contract;
- secure domains остаются no-op/guarded по общим правилам;
- parent device и sibling states не подменяют состояние exact marker.
## 11. I18n, accessibility и touch
- новые пользовательские строки для базового исправления не обязательны;
- если реализация добавляет строку вместо переиспользования существующей, RU/EN
key parity и осмысленный перевод обязательны в том же commit;
- checkbox, search input и candidate rows сохраняют keyboard focus, label и
screen-reader semantics;
- exact `entity_id` доступен как текст, а не только tooltip;
- одинаковые labels различимы без опоры только на цвет;
- Device editor остаётся desktop reference; на touch существующий поток должен
оставаться выполнимым без уменьшения touch targets.
## 12. Performance, security и observability
- candidate resolver работает линейно от количества devices + entities + live
states, сортировка — только от отфильтрованной выдачи;
- нельзя добавлять unbounded cache либо пересобирать registry snapshot на каждый
keypress;
- memoization/invalidation учитывает registry revision, live collection identity,
markers, showEntities и filter;
- filter выполняется до limit, но это не отменяет общий physical cap UI;
- entity ids показываются только уже авторизованному пользователю HA в локальном
editor; новые внешние запросы, permissions и telemetry не добавляются;
- diagnostic/test logs не должны включать пользовательские names, coordinates
или полный config; fixture использует синтетические ids;
- отдельный runtime metric не нужен, но regression должен быть виден общему
pre-beta performance gate.
## 13. Acceptance criteria и доказательства
| AC | Критерий | Обязательное доказательство |
|---|---|---|
| AC1 | Две enabled `light.*` entities одного device одновременно видны после «Показывать сущности» | unit fixture candidate resolver + browser smoke screenshot/log |
| AC2 | Поиск находит каждый канал по full entity_id, friendly/registry name, domain и parent device name | параметризованный unit test |
| AC3 | Фильтр применяется до лимита 200: точный канал находится среди >200 исходных entities | unit regression |
| AC4 | После сохранения channel A он исключён, но sibling channel B остаётся; после сохранения B существуют два exact markers | unit state transition + smoke |
| AC5 | `device:*` marker того же parent не скрывает child entity candidates и не заменяется автоматически | unit negative regression + smoke |
| AC6 | Действие каждого marker адресует только его bound entity, без sibling/parent fan-out | existing exact-toggle unit suite + targeted regression |
| AC7 | Disabled entity, disabled parent, orphaned и unverified candidate не предлагаются; removed tombstone можно добавить заново | unit status matrix |
| AC8 | Active live-only entity без registry row находится по entity_id; registry-less rendering openings не меняется | unit candidate test + source boundary assertion |
| AC9 | HA-hidden enabled entity доступна в advanced search, но HA-disabled — нет | unit registry metadata matrix |
| AC10 | При `showEntities=false` обычные device-owned entities скрыты, а devices/helpers/groups сохраняют прежнее поведение | existing + new unit/smoke regression |
| AC11 | Две одинаковые labels остаются отдельными и различимыми по entity_id, sort детерминирован | unit snapshot |
| AC12 | Конфиг не мигрирует; full/space export-import сохраняет обе bindings буквально | config round-trip unit |
| AC13 | RU/EN parity, typecheck, unit и build проходят; pre-beta smoke/performance проходят в предусмотренный процессом момент | CI/command evidence |
## 14. Тест-план
### 14.1 TypeScript unit
Синтетический authoritative registry fixture:
- device `dual_relay`;
- `light.bath` и `light.toilet` с одним `device_id`;
- sibling `sensor.dual_relay_lqi`;
- две entities с одинаковым display name;
- enabled hidden entity;
- disabled entity и entity disabled через parent;
- removed/taken bindings;
- live-only exact entity без registry row;
- более 200 шумовых entities и искомый channel после них.
Проверить AC1–AC3, AC5, AC7–AC11. Отдельно проверить последовательность
markers empty → A saved → A+B saved, неизменность исходных inputs и стабильный
tie-breaker.
Existing exact entity tests дополняются утверждением, что state и service target
двух sibling markers не пересекаются. Небезопасный domain не становится
переключаемым из-за новой discoverability.
### 14.2 Browser smoke
Расширить `demo/smoke_binding_ui.mjs` либо добавить узкий сценарий:
1. открыть Add с fixture многоканального device;
2. подтвердить, что без checkbox виден device, но не child channels;
3. включить checkbox и найти оба канала;
4. выбрать A, сохранить и повторно открыть Add;
5. убедиться, что A скрыт как taken, а B доступен;
6. сохранить B и проверить две разные bindings/позиции;
7. выполнить безопасное действие по каждой и зафиксировать разные targets;
8. проверить одинаковые labels, keyboard selection и точный secondary id.
Smoke не должен зависеть от реального MQTT broker или приватного HA diagnostic.
### 14.3 Golden и manual
Новая визуальная модель не вводится, поэтому новые golden baselines не нужны.
Если реализация изменит разметку candidate row, это расширение scope требует
обоснования и review соответствующего golden/screenshot evidence.
Manual sanity на реальном HA перед бетой:
- многоканальный MQTT/Zigbee device с двумя `light.*`/`switch.*`;
- поиск по обоим entity ids;
- отдельное размещение и переключение;
- disabled child отсутствует;
- sibling sensors/device candidates не регрессировали.
### 14.4 Команды и момент запуска
В цикле реализации:
```text
npm run typecheck
npm test
npm run build
```
Golden, smoke и performance выполняются перед бетой по release runbook. Полный
HA harness канонически выполняется в Linux CI; Windows `fcntl` не заменяется
локальным обходом.
## 15. План реализации
1. Вынести pure candidate resolution из render-класса либо создать равнозначную
чисто тестируемую authority без дублирования правил.
2. Сформировать entity universe из registry metadata и live states, применяя
канонический binding status к exact ids.
3. Развести device dedup и entity exact identity; сохранить taken/removed/edit
semantics.
4. Сделать label/sub/search/sort/limit порядок нормативным и детерминированным.
5. Подключить resolver к существующему Add dialog без изменения persisted model.
6. Добавить unit matrix и multi-channel smoke.
7. Обновить пользовательскую и архитектурную документацию, release artifacts.
## 16. Release-артефакты
Исправление пользовательски видимо. В том же class A/B commit обязательны:
- `docs/CHANGELOG.md`;
- `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — короткий сценарий отдельного размещения каналов;
- `docs/ARCHITECTURE.md` — единый binding-candidate contract;
- при изменении smoke routing — `docs/TESTING.md`;
- issue/PR evidence с unit и smoke результатами.
Новых screenshots/golden не требуется, пока внешний вид row не меняется.
Терминальные trailers продуктового commit:
```text
Issue: #109
User-Visible: yes
```
## 17. Риски и меры
| Риск | Мера |
|---|---|
| Entity universe откроет disabled rows | status matrix через единую binding authority |
| Device dedup снова скроет sibling channel | exact entity identity и отрицательный fixture с одним parent |
| Первые 200 строк скроют точный результат | filter-before-limit regression |
| Одинаковые имена станут неразличимы | обязательный entity_id и stable tie-breaker |
| Registry-less fallback создаст битую ссылку | только positive `active` status, без textual guess |
| Повторное добавление создаст дубликат | exact taken set и последовательный smoke |
| Рефакторинг сломает helpers/groups | отдельные regression cases старого always-visible contract |
| Action уйдёт во все channels device | targeted exact service-target test |
| Большой registry замедлит ввод | pure resolver, revision-aware memoization, общий performance gate |
## 18. Откат
Код откатывается обычным revert candidate resolver и его подключения. Новых
полей/версий модели нет, поэтому уже созданные exact entity markers продолжают
читаться старой версией. Откат может вернуть дефект повторного discovery, но не
должен удалять, перепривязывать или объединять сохранённые markers. Если причина
регрессии только в live-only/hidden branch, допустим узкий feature rollback этой
ветки при сохранении multi-channel registry siblings и exact taken semantics.
## 19. Принятые технические предположения
Следующие мелкие решения приняты без дополнительного вопроса владельцу:
- существующий checkbox «Показывать сущности» остаётся единственной advanced
точкой входа; новый toggle не нужен;
- HA-hidden enabled entity доступна в explicit advanced search, потому что
`hidden_by` не является `disabled_by`;
- active live-only state допустим для marker picker; это не расширяет #109 до
архитектурных openings и не закрывает #117;
- exact `entity_id` всегда показывается во secondary text для различимости;
- sort tie-breaker — exact candidate value;
- пустой filter оставляет лимит 200, непустой filter применяется до лимита;
- при исчезновении entity до Save достаточно существующего refresh/fail-safe
flow, отдельный modal не требуется;
- visual row и persisted config schema не меняются, поэтому golden и migration
не нужны.
@@ -0,0 +1,350 @@
# Issue #149 — Явный вход в настройки вида на touch
- **Issue:** https://github.com/Matysh/houseplan-card/issues/149
- **Редакция:** первая редакция для независимого ревью; статус задачи определяется
только метками issue
- **Тип / приоритет:** feature + polish / P2
- **Оценка:** пользовательская ценность 8/10 на touch; ценность для разработки
6/10; сложность 5/10, риск 7/10
- **Область:** View и киоск на touch/coarse pointer, pointer routing, внешний
фон плана, временная шестерёнка, локальные настройки размера
- **Модель данных:** server config и backend не меняются; настройки остаются
локальными для экрана в существующем `localStorage` contract
- **Связано:** `docs/UX-MODES.md`, `docs/TOUCH-SUPPORT.md`, `docs/CANVAS.md`,
`docs/WALL-THICKNESS.md`, `docs/USER-GUIDE.ru.md`
## 1. Сценарий и продуктовый контекст
**Персоны:** администратор, домочадец и гость, использующие телефон, планшет
или настенный kiosk display.
**Поверхность и момент:** в View пользователь хочет изменить локальный масштаб
значков и текста на конкретном экране.
**До → после, без терминов реализации:** сейчас вход спрятан за трёхсекундным
удержанием пустого места и его невозможно обнаружить; после изменения обычное
касание снаружи дома на пять секунд показывает шестерёнку в углу, по которой
можно открыть те же настройки, а удержание больше ничего не открывает.
Задача поддерживает J1, J2 и J3 из `docs/SCOPE.md`: вид остаётся понятным и
безопасным для всех трёх персон, включая киоск.
## 2. Решения владельца
Владелец 15.08.2026 принял defaults Q1–Q3. Каноническая запись:
https://github.com/Matysh/houseplan-card/issues/149#issuecomment-5301106992
1. Новый affordance работает только для touch/coarse pointer; desktop
mouse/hover flow не меняется.
2. Background tap показывает кнопку на 5 секунд, повторный background tap
перезапускает таймер, pan/zoom или смена пространства сразу скрывают.
3. «Вне плана» — unbounded exterior вне union видимой физической геометрии
текущего пространства. Detached porch/terrace входят в план; внутренние
holes/courtyards не являются target для показа.
Ранее в body issue владелец также зафиксировал, что кнопка присутствует в
киоске и доступна всем touch-персонам.
## 3. Термины
- **Настройки вида** — существующий локальный диалог с масштабом значков и
текста (`icon`/`font`, сейчас реализован как kiosk/per-screen size popover).
- **Общие настройки** — административный диалог, меняющий внешний вид и
server config плана. Он не является целью этой задачи.
- **Физическая геометрия** — видимый floor/paper комнат вместе с телами
room walls и видимыми независимыми физическими объектами текущего space.
- **Unbounded exterior** — единственная область дополнения этой геометрии,
соединённая с бесконечностью; замкнутые внутренние holes не входят в неё.
- **Чистый tap** — один primary touch/coarse pointer, который не был
классифицирован как pan, swipe, pinch, long-press, double-tap или действие
интерактивного объекта.
## 4. Скоуп
В задачу входят:
1. удаление открытия настроек вида долгим тапом по stage/background;
2. hit test чистого tap по unbounded exterior текущего плана;
3. временная кнопка `mdi:cog-outline` в правом нижнем углу card viewport;
4. один и тот же маршрут View и kiosk для открытия существующего локального
диалога размеров;
5. таймер 5 секунд, reset/hide lifecycle и защита от stale timers;
6. collision-safe размещение, safe-area, touch target и reduced motion;
7. RU/EN, accessibility, unit, touch smoke, golden и документация.
## 5. Не входит в задачу
- изменение desktop mouse/hover flow или показ кнопки от mouse click;
- открытие «Общих настроек», выдача прав редактора или запись в server config;
- новые поля настроек вида, изменение диапазонов icon/font scale или ключа
`localStorage`;
- постоянная кнопка в header;
- показ по касанию комнаты, стены, внутреннего двора, устройства, проёма,
decor/backdrop или любого пустого места внутри внешнего контура;
- изменение device long-press/info card, room tap, opening tap, kiosk swipe,
double-tap reset zoom, pan или pinch;
- изменение геометрии комнат/стен ради hit test;
- миграция, schema version, backend или security permission change.
## 6. Геометрический контракт background target
### 6.1. Каноническая маска
Для текущего space строится `physical footprint` в тех же world coordinates и
из той же структурной render snapshot, что и видимая архитектура:
1. объединение floor/paper всех видимых комнат, включая detached rooms,
крыльцо и террасу, смоделированные комнатами;
2. тела room walls с их текущей толщиной;
3. видимые сохранённые `partitions`, `wall_columns` и другие независимые
физические тела, которые участвуют в архитектурном слое View;
4. без скрытых `show_borders: false` объектов, если они не имеют видимого
физического представления на этой поверхности.
Backdrop, decor, labels, devices, Glow, sun, room hover/fill effects, vacuum
trail и screen-space controls не расширяют physical footprint. Они могут
владеть самим tap согласно разделу 7, но не превращают окружающий фон в дом.
### 6.2. Внешняя область и holes
Target — точка, которая:
- не лежит в physical footprint с действующим geometry epsilon;
- принадлежит unbounded exterior component его дополнения.
Следствия:
- касание на полу или теле стены не показывает кнопку;
- doorway/window cut внутри общего envelope не становится внешним target;
- замкнутый courtyard, atrium или hole внутри здания не показывает кнопку;
- detached porch/terrace сами являются plan geometry, а фон между отдельными
компонентами и вокруг них принадлежит unbounded exterior и может показать
кнопку;
- пространство внутри concave фасада считается по топологии: открытая наружу
ниша относится к exterior, полностью замкнутая — к hole;
- точка на границе/в пределах hit epsilon считается планом, чтобы дрожание
координат не вызывало кнопку поверх стены.
Если geometry union не может быть построен, affordance fail-safe не
показывается. Нельзя заменять точный тест bbox плана: он ошибочен для L-форм,
detached частей и courtyard.
### 6.3. Производительность
Physical footprint и topology вычисляются только при structural fingerprint
change (space, rooms, walls, openings, physical objects, visibility), а не на
каждый pointermove и HA state tick. На tap выполняется bounded point/topology
query по cached result. Cache не является новым persisted extent.
## 7. Владение жестом
Кнопка рассматривается только после завершения чистого primary tap:
1. pointer type — `touch` либо текущая coarse/no-hover capability;
2. режим — View; kiosk является вариантом View;
3. начало и конец не принадлежат device, room action, opening, vacuum control,
header/control/button/dialog или другому интерактивному hit target;
4. движение не превысило действующий tap threshold и не получило final
classification `pan`/`swipe`;
5. не было второго pointer/pinch, pointercancel или long-press action;
6. итоговая world point проходит раздел 6.
Hit test выполняется по release point после окончательной gesture
classification. Он не перехватывает click у объектов и не мешает их обычному
tap/long-press. `preventDefault()` не применяется шире нового чистого tap.
Длительное удержание внешнего фона может завершиться как обычный чистый tap
после release только если общий gesture recognizer не классифицирует его
иначе; оно **никогда не открывает диалог непосредственно по таймеру**. Старый
трёхсекундный `_kioskHoldTimer`/эквивалент удаляется только для stage
background; device long-press остаётся.
## 8. Кнопка и lifecycle
### 8.1. Появление
- успешный background tap показывает одну кнопку с `mdi:cog-outline`;
- если она уже видима, повторный успешный tap не создаёт вторую кнопку, а
перезапускает ровно один 5000 ms timer;
- отсчёт начинается после принятого tap;
- tap по самой кнопке немедленно отменяет timer, скрывает affordance и открывает
диалог настроек вида;
- rapid taps и stale timeout используют generation/token guard: старый timeout
не может скрыть кнопку, показанную более новым tap.
### 8.2. Немедленное скрытие
Кнопка скрывается без ожидания остатка 5 секунд при:
- начале pan, pinch или kiosk swipe;
- wheel/programmatic zoom, zoom controls, double-tap reset и Fit;
- смене space;
- выходе из View, открытии editor, размонтировании card;
- открытии любого modal/dialog, включая настройки вида;
- смене card config или structural recovery, делающей context stale;
- переходе document в hidden, чтобы после возврата не оставался просроченный
control.
Обычный HA state tick сам по себе кнопку не скрывает и не продлевает.
### 8.3. Положение и collision
- кнопка позиционируется относительно видимой области card/stage, а не world
coordinates и не двигается вместе с pan/zoom;
- базовое положение — правый нижний угол с существующим spacing token и
`env(safe-area-inset-right/bottom)`;
- минимальный hit target — 44×44 CSS px;
- если место занято видимыми kiosk dots, home-arrow, zoom/другим control,
применяется детерминированный vertical stack выше него с тем же gap;
- кнопка не перекрывает системный safe area и полностью остаётся в card
viewport в portrait/landscape;
- z-index выше stage content, но ниже modal/dialog/scrim.
### 8.4. Motion
Появление и уход используют существующие short fade/scale motion tokens и
easing редактора; отдельная длительность в config не вводится. При
`prefers-reduced-motion: reduce` состояние меняется без transition, но таймер и
доступность сохраняются.
## 9. Диалог и права
Кнопка открывает существующий per-screen диалог:
- масштаб значков;
- масштаб текста;
- Reset и Close;
- сохранение в существующий локальный ключ этого browser/device.
Этот dialog доступен администратору, домочадцу, гостю и в `kiosk: true`, потому
что меняет только представление данного экрана. Он не зависит от `_canEdit`, не
показывает Plan/Devices/Background controls и не вызывает backend save.
Административный `_settingsDialog`/«Общие настройки» остаётся под действующей
проверкой прав и не открывается новым affordance. Термин и внутреннее имя
`kiosk` могут быть переименованы для ясности, но storage compatibility должна
сохраниться.
## 10. Accessibility и i18n
- кнопка — настоящий focusable `button`, не SVG-only hit area;
- локализованные RU/EN `aria-label` и `title`: «Настройки вида» /
«View settings»;
- иконка декоративна для screen reader;
- появление кнопки не крадёт focus и не объявляется assertively;
- после её активации dialog соблюдает существующий focus trap; после закрытия
focus возвращается на кнопку, если она ещё валидна, иначе на card/stage
container по действующему dialog contract;
- auto-hide не переносит focus неожиданно: если button получил keyboard focus,
timer приостанавливается до blur либо безопасно возвращает focus перед
удалением; поскольку сам trigger touch-only, это необходимо для switch
access, а не для desktop mouse exposure;
- timer не объявляется посекундно.
## 11. Модель данных и compatibility
- `ServerConfig`, space/room data и backend validation не меняются;
- существующий localStorage key и значения icon/font scale читаются и пишутся
без миграции;
- visibility, timer и geometry cache не сериализуются;
- предыдущая версия после rollback продолжает читать локальные масштабы;
- простой background tap без открытия/изменения dialog ничего не пишет;
- kiosk и normal View используют один per-screen value, как и до изменения.
## 12. Acceptance criteria
1. На touch/coarse View чистый tap в unbounded exterior показывает одну
шестерёнку в правом нижнем углу на 5 секунд.
2. Повторный валидный tap перезапускает полный 5-секундный срок без duplicate
control и без риска от stale timeout.
3. Tap по кнопке открывает локальные настройки icon/font scale всем персонам,
включая kiosk, и не открывает/не даёт доступ к «Общим настройкам».
4. После 5 секунд кнопка исчезает; pan, pinch, swipe, любой zoom, Fit, смена
space/mode, dialog и unmount скрывают немедленно.
5. Tap комнаты, стены, detached porch/terrace, внутреннего courtyard/hole,
устройства, проёма или control не показывает кнопку и сохраняет своё
обычное действие.
6. Concave exterior и фон между detached физическими компонентами корректно
показывают кнопку; bbox-эвристика не используется.
7. Старый трёхсекундный background long-press не открывает dialog. Device
long-press, double-tap reset, kiosk swipe и pan/pinch не регрессируют.
8. Desktop mouse/hover flow не меняется и mouse click кнопку не показывает.
9. Кнопка 44×44+, учитывает safe-area/collisions, не выходит за viewport и
использует reduced-motion.
10. RU/EN label, keyboard/switch focus и dialog focus trap проходят проверку.
11. Ни показ кнопки, ни изменение per-screen размеров не пишут server config;
старые localStorage values совместимы.
12. Geometry query использует structural cache и не выполняет boolean union на
pointermove или HA tick.
## 13. Проверки и доказательства
### Unit
- unbounded exterior classification для rectangle, L-shape, concave niche,
courtyard/hole и двух detached components;
- physical mask с разной толщиной стен, partition/column и wall visibility;
- gesture classifier: clean tap против pan/pinch/swipe/double tap/object hit;
- timer reset, generation guard и все immediate-hide causes;
- permission boundary: affordance вызывает только per-screen dialog.
### Touch browser smoke
- phone/Companion View: exterior tap → gear → dialog → icon/font change;
- wall-tablet kiosk: тот же flow, auto-hide, repeated tap;
- tap на floor/wall/courtyard/detached porch/device/opening;
- pan, pinch, zoom controls, double-tap reset, multi-space swipe и space change;
- device long-press по-прежнему открывает info card;
- portrait/landscape, safe-area и collision с другими controls;
- guest/read-only session не получает editor/general settings.
### Golden
- View и kiosk с видимой кнопкой в phone portrait и wall-tablet landscape;
- collision/stack state с существующим control;
- light/dark theme и reduced-motion final state.
Golden, smoke и performance запускаются в release gate перед бетой; цикл
реализации — `typecheck`, `unit`, `build` по процессу.
## 14. Release-артефакты
В том же user-visible коммите реализации обязательны:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — обнаруживаемый маршрут к настройкам вида;
- `docs/UX-MODES.md` — View/kiosk больше не используют long-press;
- `docs/TOUCH-SUPPORT.md` — background target и gesture ownership;
- при необходимости `docs/CANVAS.md` — только ссылка на определение physical
exterior, без превращения его в stored canvas boundary;
- RU/EN locale files;
- touch smoke и принятые golden/baselines;
- `docs/TESTING.md` — замена long-press smoke новым flow.
Backend/security release artifacts не требуются: права и wire format не
меняются. Если реализация потребует server write, задача возвращается владельцу.
## 15. Риски и rollback
Риски: случайный показ после pan, неверный bbox hit test, stale timer, конфликт
с kiosk swipe и путаница с административными настройками. Их закрывают final
gesture classification, topology test, generation guard и явная permission
граница.
Rollback удаляет временную кнопку и восстанавливает предыдущий UI-код без
миграции. LocalStorage и server config остаются совместимыми; возвращать старый
long-press при rollback допустимо только вместе с откатом всего user-visible
изменения.
## 16. Принятые технические предположения
1. Канонический footprint переиспользует результат текущей wall/paper geometry
и `physicalBodies`, а не строит параллельную модель по DOM nodes.
2. Открытые door/window slots не становятся background targets, потому что
берётся unbounded component дополнения общего physical envelope.
3. Saved unfinished room draft не входит в View footprint, если View его не
рисует; правило «видимая физическая геометрия» важнее наличия записи в config.
4. Existing animation token определяет фактическую short transition duration;
продуктовый контракт фиксирует только 5000 ms visibility window.
5. При keyboard/switch focus auto-hide приостанавливается до blur: это
минимальная accessible гарантия без показа affordance от desktop mouse.
+2 -2
View File
@@ -1,6 +1,6 @@
# Спецификации задач P1 и P2
Актуально на 2026-08-15.
Актуально на 2026-08-14.
GitHub Issues и GitHub Projects (v2) остаются единственным каноническим backlog проекта. Этот каталог содержит развёрнутые ТЗ: каждое ТЗ ссылается на issue, а issue — на соответствующий файл. Статус, приоритет и факт завершения меняются только в GitHub.
@@ -82,11 +82,11 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#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) |
| [#109](https://github.com/Matysh/houseplan-card/issues/109) Отдельные markers для каналов многоканального HA-устройства | [109-multichannel-entity-binding.md](109-multichannel-entity-binding.md) |
| [#122](https://github.com/Matysh/houseplan-card/issues/122) Изометрический режим Stage 2: скрытый режим и визуальная полировка | [122-isometric-stage2.md](122-isometric-stage2.md) |
| [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) |
| [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) |
| [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) |
| [#149](https://github.com/Matysh/houseplan-card/issues/149) Явный вход в настройки вида на touch | [149-touch-view-settings-affordance.md](149-touch-view-settings-affordance.md) |
## Правило актуализации