mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 03:38:47 +00:00
239 lines
12 KiB
Markdown
239 lines
12 KiB
Markdown
# Issue #103 — текущее и ожидаемое состояние в Toggle confirmation
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/103
|
||
- **Статус документа:** принято ревью ТЗ; реализация разрешена из `S5-ready`
|
||
- **Приоритет:** P3
|
||
- **Тип:** feature/polish, обычный трек
|
||
- **Пользовательское изменение:** да
|
||
|
||
## 1. Контекст
|
||
|
||
Универсальный toggle из #94 уже строит один `ResolvedToggleIntent` для hint,
|
||
confirmation guard и фактической service call. При `tap_confirm` текущий диалог
|
||
показывает только имя цели. Пользователь не видит исходное состояние и
|
||
направление команды, особенно у cover/valve и смешанных групп.
|
||
|
||
Это изменение встречает любую персону House Plan в View на desktop, companion
|
||
app или wall panel в момент короткого нажатия на маркер с включённым
|
||
`tap_confirm`; поверхность относится к fully-supported safe device actions.
|
||
|
||
#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` + `open` | Закрыто | Открыто |
|
||
| cover `open` + `close` | Открыто | Закрыто |
|
||
| cover `opening/closing` + `stop` | Открывается/Закрывается | Остановлено |
|
||
| 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 — **unit + browser smoke**.
|
||
2. Expected line строго соответствует `nextEffect` — **unit**.
|
||
3. Power, cover, valve, virtual light и group имеют локализованные формулировки — **unit + RU/EN browser smoke**.
|
||
4. Partial group явно показывает skipped count и не обещает их изменение — **unit**.
|
||
5. `toggle` сообщает, что результат определит HA — **unit**.
|
||
6. No-operation intent не открывает confirmation — **unit + browser smoke**.
|
||
7. Confirm выполняет заново разрешённый current intent — **browser smoke**.
|
||
8. Изменившийся target set отменяет actuation и показывает прежний toast — **browser smoke**.
|
||
9. Desktop/mobile layout, keyboard и screen-reader order не регрессируют — **narrow browser smoke + code review**.
|
||
10. Run и другие confirmations сохраняют прежнее содержимое — **existing run smoke + code review**.
|
||
|
||
## 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.
|