docs: specify toggle confirmation states

Issue: #103
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-19 03:12:55 +03:00
parent e88c23b8ee
commit a449edc545
2 changed files with 241 additions and 1 deletions
+234
View File
@@ -0,0 +1,234 @@
# Issue #103 — текущее и ожидаемое состояние в Toggle confirmation
- **Issue:** https://github.com/Matysh/houseplan-card/issues/103
- **Статус документа:** готово к будущей реализации; issue остаётся на `S3-spec`
- **Приоритет:** P3
- **Тип:** feature/polish, обычный трек
- **Пользовательское изменение:** да
## 1. Контекст
Универсальный toggle из #94 уже строит один `ResolvedToggleIntent` для hint,
confirmation guard и фактической service call. При `tap_confirm` текущий диалог
показывает только имя цели. Пользователь не видит исходное состояние и
направление команды, особенно у cover/valve и смешанных групп.
#103 расширяет только содержимое существующего confirmation. Resolver, target
selection, кнопки и safety re-resolve остаются прежними.
## 2. Пользовательский результат
Одиночная цель:
> Переключить «Лампа в прихожей»?
>
> Текущее состояние: Выключено
>
> После переключения: Включено
Группа:
> Текущее состояние: включено 2 из 4
>
> После переключения: все выключены
Строки являются обычным доступным текстом, а не цветом, и читаются до кнопок
Cancel/Confirm.
## 3. Цели
1. Объяснить текущее и ожидаемое состояние каждой исполняемой toggle-команды.
2. Использовать только `ResolvedToggleIntent`, без UI-domain эвристики.
3. Не обещать результат для skipped/unknown целей.
4. Сохранить re-resolve перед service call и target-set race protection #94.
## 4. Не входит в задачу
- изменение resolver, command или service;
- прогноз scripts/scenes и произвольных actions;
- история состояний;
- confirmations удаления/unlock/run и общий dialog redesign #32;
- live-анимация состояния в открытом диалоге;
- изменение `tap_confirm` schema.
## 5. Источник данных
Confirmation snapshot строится из `ResolvedToggleIntent`:
- `targets[].state` и `targets[].name`;
- `kind` и `semantics`;
- `nextEffect`;
- `skippedTargets`;
- stable identity из `sameToggleOperationTargets()`.
`src/device-toggle.ts` остаётся владельцем line selection. Допустимо расширить
`formatToggleIntent()` либо добавить рядом pure `formatToggleConfirmation()`, но
`houseplan-card.ts` не выводит next state по domain самостоятельно.
State label берётся через HA formatter, когда state object доступен. Raw state
допустим только как безопасный fallback; неизвестный будущий результат никогда
не подменяется уверенным On/Off.
## 6. Нормативная матрица
| Intent | Текущее состояние | После переключения |
| --- | --- | --- |
| power `off` + `turn-on` | Выключено | Включено |
| active power + `turn-off` | HA-formatted current | Выключено |
| cover `closed/closing` + `open` | Закрыто/Закрывается | Открыто |
| cover `open/opening` + `close` | Открыто/Открывается | Закрыто |
| cover + `stop` | HA-formatted current | Остановлено |
| valve + `open/close` | Закрыто/Открыто | Открыто/Закрыто |
| group, все off | Все выключены | Все включены |
| group, есть active | Включено N из M | Все выключены |
| partial group | доступное подмножество + Недоступно N | результат только command targets |
| `toggle` | HA-formatted current | Состояние определит Home Assistant |
| no operation | confirmation не открывается | — |
Direction всегда следует `nextEffect`; UI не пересчитывает её из текущей строки.
Для группы denominator результата равен числу фактических `targets`, а skipped
выводятся отдельно. Формулировка не обещает, что unavailable/disabled/missing
entities изменятся.
## 7. Dialog state и race
`_tapConfirm` расширяется структурированным snapshot, а не одним HTML string:
```ts
interface TapToggleConfirmation {
kind: 'toggle';
title: string;
lines: string[];
initialIntent: ResolvedToggleIntent;
deviceId: string;
exec: () => void;
}
```
Минимальный обязательный контракт:
- при открытии title/lines фиксируются из initial intent;
- при Confirm текущий device и intent разрешаются заново;
- если operation targets изменились, service call отсутствует и показывается
существующий `toast.tap_target_changed`;
- если targets прежние, выполняется актуальное direction/command, даже если
state изменился после открытия;
- dialog snapshot не обязан live-обновлять строки.
`run` confirmation продолжает использовать нынешнюю простую форму. Общий union
не должен заставлять run/delete dialogs притворяться toggle.
## 8. UX, i18n и accessibility
- title сохраняет нынешнее «Переключить …?»;
- current/expected/skipped — отдельные строки/paragraphs;
- long friendly name и group text переносятся без horizontal scroll;
- accessible DOM order: title → current → expected → skipped → buttons;
- смысл не выражается только иконкой/цветом/стрелкой;
- narrow mobile footer сохраняет две доступные кнопки;
- keyboard focus и Escape/scrim contract не меняются.
Минимальные новые RU/EN keys:
- `confirm.current_state`;
- `confirm.expected_state`;
- `confirm.group_current`;
- `confirm.group_all_on` / `confirm.group_all_off`;
- `confirm.unavailable_targets`;
- `confirm.expected_by_ha`;
- state/effect labels, которых ещё нет в общей toggle vocabulary.
Не вводятся plural rules; строки следуют текущему counter-safe style.
## 9. Совместимость и безопасность
- stored `tap_confirm` и config не меняются;
- no-op по-прежнему тихий и не открывает modal;
- secure/disabled/missing filtering остаётся resolver-owned;
- confirmation не исполняет команду из initial snapshot;
- operation target identity проверяется перед actuation;
- virtual-light intent использует тот же current/expected formatter и current
backend snapshot, не создавая HA service;
- никаких новых permissions или network requests.
## 10. Acceptance criteria
1. Toggle confirmation для operation показывает current и expected lines.
2. Expected line строго соответствует `nextEffect`.
3. Power, cover, valve, virtual light и group имеют локализованные формулировки.
4. Partial group явно показывает skipped count и не обещает их изменение.
5. `toggle` сообщает, что результат определит HA.
6. No-operation intent не открывает confirmation.
7. Confirm выполняет заново разрешённый current intent.
8. Изменившийся target set отменяет actuation и показывает прежний toast.
9. Desktop/mobile layout, keyboard и screen-reader order не регрессируют.
10. Run и другие confirmations сохраняют прежнее содержимое.
## 11. План тестирования
### Unit
- pure formatter для каждого `ToggleNextEffect`;
- single power/cover/valve/virtual-light;
- all-off, mixed и partial group;
- formatted current state и raw fallback;
- unknown `toggle` result;
- no-operation returns no confirmation lines;
- EN/RU placeholder parity.
### Integration/browser
- confirmation DOM order на desktop и narrow mobile;
- initial snapshot + state changes + same targets → current command executes;
- changed targets → zero service calls + toast;
- skipped target is not included in command/result promise;
- keyboard focus, Escape, Cancel and scrim;
- run confirmation unchanged.
### Регрессия
- `test/device-toggle.test.mjs`;
- existing HA-controls/toggle smoke;
- typecheck, full unit и build.
Golden не требуется, если modal reflow покрыт narrow render smoke и не меняет
принятый внешний layout. При необходимости добавляется одна deterministic dialog
scene без переакцептации несвязанных baseline.
## 12. План реализации
1. Добавить pure confirmation formatter рядом с resolver.
2. Расширить `_tapConfirm` discriminated state для toggle.
3. Отрисовать semantic lines в текущем dialog.
4. Добавить RU/EN strings и parity tests.
5. Проверить race contract и existing confirmations.
## 13. Документация и release-артефакты
- оба changelog получают user-visible пункт;
- `docs/USER-GUIDE.ru.md` показывает current→expected confirmation;
- `docs/TESTING.md` получает group/race/narrow dialog matrix;
- RU/EN dictionaries меняются в одном implementation commit;
- screenshot/golden — только для точечной modal scene при необходимости;
- backend, migration, performance profile и security artifact не требуются.
## 14. Риски и откат
| Риск | Мера |
| --- | --- |
| Dialog обещает не ту команду | nextEffect-only formatter |
| Initial snapshot исполняется после race | mandatory re-resolve |
| Skipped входят в denominator | separate targets/skipped assertions |
| UI дублирует resolver | pure device-toggle formatter |
| Long group ломает mobile | narrow viewport smoke |
Откат возвращает `_tapConfirm` к одному title string. Config и resolver не
меняются, поэтому data rollback отсутствует.
## 15. Принятые технические предположения
- первая версия показывает snapshot и не live-обновляет открытый dialog;
- current state использует HA formatter при наличии;
- `stop` локализуется как честный ожидаемый effect, не как конечная позиция;
- group result описывает только фактические command targets.
+7 -1
View File
@@ -1,4 +1,4 @@
# Спецификации задач P1 и P2
# Спецификации задач
Актуально на 2026-08-17.
@@ -100,6 +100,12 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#174](https://github.com/Matysh/houseplan-card/issues/174) Связанный виртуальный источник следует реальному контроллеру | [174-linked-virtual-light-controller.md](174-linked-virtual-light-controller.md) |
| [#178](https://github.com/Matysh/houseplan-card/issues/178) Выбор сущности для действия «Переключить состояние» | [178-toggle-entity.md](178-toggle-entity.md) |
## P3
| Issue | ТЗ |
|---|---|
| [#103](https://github.com/Matysh/houseplan-card/issues/103) Состояния в Toggle confirmation | [103-toggle-confirmation-state.md](103-toggle-confirmation-state.md) |
## Правило актуализации
При изменении продуктового решения сначала обновляется соответствующее issue, затем ТЗ. Реализация не считается завершённой только по наличию кода: нужны выполненные acceptance criteria, предусмотренная ТЗ проверка и актуальный статус Project v2.