mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a28a4b7065 | ||
|
|
2d5f0a5f0f |
@@ -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.
|
||||
@@ -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) |
|
||||
|
||||
## Правило актуализации
|
||||
|
||||
|
||||
Reference in New Issue
Block a user