mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 06:08:59 +00:00
213 lines
16 KiB
Markdown
Executable File
213 lines
16 KiB
Markdown
Executable File
# ТЗ #402 — Подтверждение опасного действия живёт вне зависимости от ветки render()
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/402
|
||
- Приоритет: P1, bug (регресс против поведения до #32); полный трек — класс A
|
||
- Ревизия: 2 (2026-08-31) — по SPEC-REVIEW-402-r1 (H1: touch/kiosk; M1: причина
|
||
исключения соседних подтверждений)
|
||
|
||
## Сценарий
|
||
|
||
Хозяин ставит карточку в первый раз: пространств ещё нет, идёт онбординг. В
|
||
списке сохранённых на сервере планов лежит чужой, ошибочно загруженный файл —
|
||
он жмёт корзину. Ничего не происходит. Ни диалога, ни удаления, ни сообщения:
|
||
кнопка мертва, и понять почему нельзя. До #32 здесь спрашивал браузерный
|
||
`confirm()`, и удаление работало.
|
||
|
||
## Что человек увидит до и после
|
||
|
||
**До**: в состоянии «нет ни одного пространства» (а также при недоступном
|
||
`fixed_floor` и при потерянном пространстве) любое опасное действие молча не
|
||
делает ничего; если подтверждение было открыто до перехода в такое состояние —
|
||
диалог исчезает сам, а действие остаётся ни выполненным, ни отменённым.
|
||
**После**: подтверждение спрашивается и в этих состояниях; если карточка вообще
|
||
ничего не рисует, действие отклоняется явно, а не подвисает.
|
||
|
||
## Проблема и контракт
|
||
|
||
`HpConfirmController` (`src/danger-confirm.ts:36`) корректен: `cancel()`
|
||
резолвит промис `false`, `resolve()` сверяет токен, новый запрос отменяет
|
||
предыдущий. Промис виснет не потому, что контроллер теряет решение, а потому
|
||
что решение неоткуда взяться: оно приходит событием `hp-confirm-decision` от
|
||
элемента `<hp-confirm>`, а элемента в DOM нет.
|
||
|
||
`render()` (`src/houseplan-card.ts:11165`) — цепочка ранних `return`, и
|
||
`hp-confirm` стоит только в её конце (`:11872`):
|
||
|
||
| Строка | Ветка | Рисует |
|
||
|---|---|---|
|
||
| `:11166` | нет `_config`/`hass` | `nothing` |
|
||
| `:11170` | языковой гейт `cold` | загрузчик языка |
|
||
| `:11171` | языковой гейт `warm` | `noChange` |
|
||
| `:11192` | `fixed_floor` pending | своя карточка |
|
||
| `:11203` | `fixed_floor` invalid | своя карточка |
|
||
| `:11216` | `!model.length` (онбординг) | своя карточка |
|
||
| `:11247` | `!space` | `nothing` |
|
||
| `:11872` | основная | **единственное место с `hp-confirm`** |
|
||
|
||
Удаление сохранённого плана вызывается из онбординга
|
||
(`src/houseplan-onboarding-runtime.ts:273` → `_deleteServerPlan`, который
|
||
ждёт `_confirmDanger`), то есть буквальный сценарий issue лежит в ветке
|
||
`:11216`.
|
||
|
||
**Воспроизведено исполнением** (браузер, реальная карточка): очищаю `spaces`,
|
||
вызываю `_confirmDanger({...})` →
|
||
|
||
```
|
||
controllerHasRequest: true
|
||
confirmDialogInDom: false
|
||
promiseSettledOrDialogShown: false (ожидание 200 мс)
|
||
```
|
||
|
||
**Вторая половина того же дефекта** (в issue не описана, найдена при разборе):
|
||
уже открытое подтверждение исчезает, если карточка переходит в любую раннюю
|
||
ветку — потеряла пространство, ушла в `fixed_floor` pending. Пользователь
|
||
видит, как диалог пропал сам; вызывающий остаётся с неразрешённым промисом.
|
||
Поэтому «добавить рендер в ветку онбординга» — недостаточное решение: оно
|
||
чинит один вход и оставляет класс.
|
||
|
||
**Контракт**: `hp-confirm` не принадлежит ни одной ветке `render()`. Он
|
||
рендерится всегда, когда карточка рисует хоть что-то, и переживает смену
|
||
ветки. Разделение ответственности прежнее: контроллер владеет состоянием и
|
||
токеном, вызывающий — ревалидацией после `await` (введена #32 и не меняется).
|
||
|
||
**Граница**: две ветки рисуют «ничего» осознанно — `noChange` языкового гейта
|
||
(`:11171`, Lit-сигнал «не трогай DOM», его нельзя обернуть в шаблон) и
|
||
`nothing` до инициализации (`:11166`). В них подтверждение показать
|
||
невозможно, и правильное поведение — **отклонить запрос сразу** (промис
|
||
резолвится `false`), а не оставить его висеть. Пользователь в этот момент
|
||
ничего не нажимал: карточка ещё не готова.
|
||
|
||
## Скоуп / не-скоуп
|
||
|
||
**В скоупе**: структура `render()` в `src/houseplan-card.ts` (вынос
|
||
подтверждения из цепочки), поведение `_confirmDanger` в неготовых состояниях,
|
||
смоки и мутанты.
|
||
|
||
**Не в скоупе**: содержимое и оформление диалога (#32), ревалидация после
|
||
`await` у вызывающих (#32), роль `alertdialog` и `aria-describedby` (#406).
|
||
|
||
**`_tapConfirm` и `_vacCalConfirm` — не в скоупе, и причина не та, что я
|
||
написал в ревизии 1.** Они рендерятся в той же финальной ветке `render()`
|
||
(`:11802-11878`), что и `_dangerConfirm`, — «свои ветки» было неверным
|
||
утверждением. Настоящих причин две, и обе по существу:
|
||
|
||
1. У них **нет промиса**: `_tapConfirm` хранит синхронный `exec()`, который
|
||
вызывается прямо из обработчика кнопки, а `_vacCalConfirm` — это
|
||
информационный диалог с закрытием по `hp-close`. Ни один вызывающий не
|
||
ждёт разрешения, поэтому «висящего промиса» — самого дефекта этой задачи —
|
||
у них не бывает.
|
||
2. Их точки входа недостижимы из ранних веток: тап по маркеру и калибровка
|
||
пылесоса требуют отрисованного плана, то есть той самой основной ветки.
|
||
|
||
Из этого следует практическое требование к реализации: при выносе
|
||
`hp-confirm` из цепочки **соседние два блока остаются на месте**. Утащить их
|
||
за компанию — значит без нужды поменять поведение диалогов, для которых эта
|
||
задача ничего не обещает.
|
||
|
||
## UX
|
||
|
||
Видимых изменений в оформлении нет. Меняется только то, что подтверждение
|
||
появляется там, где раньше действие молча не работало.
|
||
|
||
## Touch и kiosk
|
||
|
||
`docs/TOUCH-SUPPORT.md` § Safety floor запрещает «best effort» в трёх вещах,
|
||
и одна из них — обход подтверждения разрушающего действия. Сейчас на touch
|
||
этот пол пробит ровно так же, как на мыши: в ветке онбординга подтверждения
|
||
нет вовсе, то есть действие либо не работает, либо (если бы работало) шло бы
|
||
без спроса. Задача поднимает пол обратно на всех вводах сразу.
|
||
|
||
Поведение по вводам после правки одинаково и специальной работы не требует:
|
||
переносится **место рендера** одного и того же компонента `hp-confirm`,
|
||
разметка, размеры целей нажатия, поведение scrim и футера (#32 §6.2) не
|
||
меняются ни на строку. Kiosk отдельного пути не имеет — он рисуется той же
|
||
основной веткой, где подтверждение и так работало.
|
||
|
||
Доказательство — AC8: смок из плана прогоняется в touch-эмуляции, то есть
|
||
проверяет ветку онбординга не только мышью.
|
||
|
||
## Модель данных и миграция
|
||
|
||
Не применимо.
|
||
|
||
## i18n
|
||
|
||
Новых строк нет.
|
||
|
||
## Критерии приёмки
|
||
|
||
- **AC1**. В состоянии «нет ни одного пространства» запрос подтверждения
|
||
показывает диалог; «Отмена» резолвит промис `false`, подтверждение — `true`.
|
||
Доказательство: браузерный смок на реальной карточке.
|
||
- **AC2**. Тот же инвариант в ветках `fixed_floor` pending и invalid и в ветке
|
||
`!space`. Доказательство: тот же смок, три ветки.
|
||
- **AC3**. Удаление сохранённого плана из онбординга доходит до конца:
|
||
подтверждение показано, после согласия план удалён. Доказательство: смок на
|
||
буквальном сценарии issue (`_deleteServerPlan`).
|
||
- **AC4**. Открытое подтверждение переживает смену ветки: карточка уходит в
|
||
`fixed_floor` pending при открытом диалоге — диалог остаётся, решение
|
||
по-прежнему резолвит промис. Доказательство: смок.
|
||
- **AC5**. В неготовых состояниях (`!_config || !hass`, языковой гейт `warm`)
|
||
запрос отклоняется немедленно: промис резолвится `false`, DOM не трогается.
|
||
Доказательство: юнит/смок с проверкой, что `noChange` из гейта не подменён
|
||
шаблоном.
|
||
- **AC6**. Существующее поведение основной ветки не изменилось: начальный фокус
|
||
на «Отмена», Esc отменяет, замена запроса отменяет предыдущий, вложенный
|
||
диалог не ломается. Доказательство: `demo/smoke_danger_confirmation.mjs`
|
||
остаётся зелёным без правок его утверждений.
|
||
- **AC7**. Бюджет initial не растёт (правка структурная). Доказательство:
|
||
`npm run bundle:budget` до и после.
|
||
- **AC8**. На touch подтверждение в ветке онбординга работает так же, как
|
||
мышью: диалог появляется, тап по «Отмена» резолвит промис `false`, тап по
|
||
scrim не проваливается в план. Доказательство: тот же смок в
|
||
touch-эмуляции (`hasTouch`), как это делают существующие touch-смоки.
|
||
- **AC9**. `_tapConfirm` и `_vacCalConfirm` остались в основной ветке и их
|
||
поведение не изменилось. Доказательство: существующие смоки тапа и
|
||
калибровки зелёные без правок их утверждений.
|
||
|
||
## План автотестов
|
||
|
||
**Browser smoke** (`demo/smoke_danger_confirmation.mjs` — дополнение):
|
||
|
||
1. Пустая модель → `_confirmDanger` показывает диалог, «Отмена» → `false`
|
||
(AC1).
|
||
2. `fixed_floor` pending / invalid / `!space` → то же (AC2).
|
||
3. Онбординг: клик по корзине плана → диалог → согласие → план удалён (AC3).
|
||
4. Диалог открыт → карточка уходит в `fixed_floor` pending → диалог на месте,
|
||
решение резолвит промис (AC4).
|
||
5. Языковой гейт `warm` → запрос резолвится `false` сразу, `render()`
|
||
по-прежнему возвращает `noChange` (AC5).
|
||
6. Тот же сценарий (1) в touch-эмуляции: диалог появляется, тап по «Отмена»
|
||
резолвит `false`, тап по scrim не доходит до плана (AC8).
|
||
7. Один `hp-confirm` в DOM — не два (риск двойного рендера ниже).
|
||
|
||
**Мутант** (`scripts/mutation-gate.mjs`):
|
||
|
||
- `danger-confirm-back-into-the-branch`: вернуть `hp-confirm` внутрь основной
|
||
ветки → смок AC1 красный.
|
||
|
||
## Риски
|
||
|
||
- **`noChange` нельзя оборачивать.** Оборачивание сигнала в шаблон сломает
|
||
оптимизацию Lit и, вероятно, вызовет лишние перерисовки. Смягчение: ветки
|
||
`noChange`/`nothing` остаются нетронутыми, для них — явный отказ запроса;
|
||
AC5 это фиксирует.
|
||
- **Двойной рендер диалога.** Если подтверждение окажется и в общей обёртке, и
|
||
в основной ветке, получится два элемента и два источника событий. Смягчение:
|
||
смок проверяет ровно один `hp-confirm` в DOM.
|
||
- **Отказ в неготовом состоянии может удивить вызывающего.** Действие
|
||
отклоняется без участия пользователя. Смягчение: это состояние наступает до
|
||
того, как карточка вообще отрисована, то есть нажать было физически нельзя;
|
||
вызывающие уже обрабатывают `false` как «пользователь отказался».
|
||
|
||
## Откат
|
||
|
||
Возврат `hp-confirm` в основную ветку — одно перемещение блока. Данные
|
||
пользователя не затрагиваются.
|
||
|
||
## Release-артефакты
|
||
|
||
- `docs/CHANGELOG.md` / `docs/CHANGELOG.ru.md`: пункт о том, что подтверждения
|
||
работают и до создания первого пространства (User-Visible: yes).
|
||
- Скриншоты не меняются: в статике диалог не открыт.
|