36 KiB
#493 — Ограниченный picker и надёжный lifecycle сводной панели
Issue: #493.
Связанные задачи: #437,
#490.
Ветка: issue/493-summary-panel-hardening; база спецификации:
dev@71aeb860.
Редакция: 2026-09-09. Полный трек, P2/bug.
Канонический статус — метка issue, не заголовок этого документа.
1. Сценарий
Администратор дома открывает настройки сводной панели в карточке House Plan, где уже может быть до 200 показателей и тысячи доступных HA-сущностей. Он выбирает источник показателя, меняет локальный размер значков/текста, повторно открывает или пересоздаёт карточку, оптимизирует/копирует/импортирует план. Домочадец либо пользователь киоска меняет только локальный показ и размеры. На телефоне любой доступный ему вариант формы должен помещаться в экран.
Это продолжение утверждённого исключения #437 из docs/SCOPE.md: надёжный
read-only overview (J1) и сохранение настройки дома при его изменении (J6), а
не расширение House Plan до произвольного dashboard framework.
2. Что человек увидит до и после
До: большая форма может на секунды зависать, потому что полный список
сущностей повторяется в каждой строке; повторный setConfig визуально сбрасывает
сохранённые размеры; после ухода со страницы или смены пользователя может
остаться старый черновик; на узком русском интерфейсе проценты и действия
обрезаются; Optimize имеет отличающийся серверный контракт сохранения панели.
После: у каждой строки остаётся понятный выбор источника, но открыт и отрисован только один ограниченный поисковый список; локальные размеры не скачут; новая сессия/пользователь/набор прав не наследует чужой диалог; форма полностью доступна на узком экране; все штатные способы изменения плана сохраняют либо явно заменяют панель по одной документированной матрице.
3. Проблема и подтверждённые причины
Аудит и повторное чтение текущего dev подтверждают пять независимых пробелов:
summary-panel-editor.tsсортирует весьhass.statesпри каждом render и создаёт полный набор<option>внутри каждой value-строки. Диагностический случай 100 строк × 1012 states создал 101510 option-узлов; открытие заняло около 2982 ms, один ввод — около 829 ms. Эти числа не являются CI budget, но доказывают архитектурный ростrows × states.houseplan-card.setConfig()повторно записывает_kioskScaleиз старого общегоhouseplan_card_kiosk_v1, аLoadedSummaryPanelRuntime.loadLocal()пропускает уже известный preference key. Поэтому local 200%/150% фактически становятся 100%/100%, хотя правильная запись остаётся в localStorage._leaveCardRoute()не завершает summary dialog, аdisconnect()снимает только clock timer. Диалог, draft и результаты незавершённой lazy/async операции не привязаны к generation пользователя/соединения..summary-local-sizesвсегда требует три колонкиmax-content + minmax(140px,1fr) + 48px. При доступной ширине 346 px русская форма требует около 389 px, и значение процента выходит за экран.config/setвызываетpreserve_summary_panel_namespace()иvalidate_summary_panel_references(), аplan/optimize— нет. Остальные полные/частичные writers используют разные механизмы, но единая проверяемая матрица для namespace и семантики отсутствует.
#490 закрывает только authoritative lost-ACK recovery и live dependency invalidations. Она намеренно не исправляет перечисленные пути и не дублирует эту задачу.
4. Область изменения
В scope:
- один активный ограниченный picker источника с общим поисковым индексом;
- сохранение фактически применённых local scales при повторном same-key
setConfig, remount и reload; - завершение dialog/draft/active picker и инвалидирование закрытого runtime state при route leave, disconnect/reconnect, смене HA user или прав;
- адаптивная полная и local-only форма на 320/390 px, RU/EN и увеличенном системном тексте;
- единый backend helper/contract для обычной записи
config/setи Optimize; - явная writer matrix для config/set, Optimize, full/space/plan-only import, Optimize/Import Undo, создание/копирование/удаление пространства;
- автоматические unit/backend/browser/performance доказательства регрессий.
Не в scope:
- новые типы значений, формулы, действия, произвольные карточки или больше лимитов 10 блоков × 20 строк;
- изменение содержимого или компоновки read-only overlay;
- перенос local preferences между браузерами, пользователями или карточками;
- новая настройка, ручная миграция либо изменение server config/store version;
- изменение правил доступа HA и возможность редактировать shared panel в kiosk;
- исправления lost-ACK/live invalidation, уже принятые в #490;
- переработка всех entity-picker в House Plan;
- смягчение schema, geometry, marker-link или revision guards других writers.
5. Термины и обязательные инварианты
- index — не-DOM массив доступных текущему
hassentity id и friendly name, нормализованный для поиска и отсортированный детерминированно; - active picker — единственная открытая поверхность выбора источника во всей summary form;
- bounded results — ограниченное число строк результата, независимо от общего числа HA states;
- local identity — stable preference key плюс HA user/host/route/card slot;
- lifecycle generation — токен текущего соединённого пользователя и набора прав; завершение старой async-операции не может менять новую generation;
- ordinary writer — конфигурационная запись поверх текущего документа;
- authoritative restore — явная полная замена config из full archive либо точное восстановление ранее записанного backup.
Инварианты:
- число DOM-строк picker не зависит от
rows × states; - через поиск доступна каждая читаемая
hass.statesentity, в том числе не размещённая на плане; одинаковые friendly names различаются по entity id; - выбранный старый broken source остаётся видимым и round-trip-ится, пока пользователь явно не заменит его;
- local scale после разрешения identity имеет одного владельца — summary local preference; старый kiosk key только одноразовый seed при отсутствии записи;
- dialog/draft принадлежат ровно одной lifecycle generation;
- omission namespace обычным writer не означает удаление; явный
blocks: []остаётся единственным пустым version-1 составом; - ни один writer не реконструирует весь
settingsиз известного allowlist и не теряет независимые известные/будущие поля.
6. UX ограниченного выбора источника
6.1 Закрытая строка
В каждой value-строке вместо постоянно наполненного native select находится
одно поле выбора, показывающее текущий источник: системное имя либо
friendly name — entity_id. Пустой и старый broken source используют уже
принятые тексты/предупреждения #437. Изменение остаётся частью общего draft и
фиксируется только кнопкой «Сохранить» всей формы.
6.2 Открытие и поиск
Click/tap/Enter/Space открывает picker только этой строки. Другой открытый picker закрывается. Внутри находятся:
- поле поиска по friendly name и entity id;
- три системных показателя;
- не более 100 первых совпадений entity в стабильном порядке;
- текущий broken source отдельной строкой, даже если его нет в index;
- подсказка уточнить поиск, если совпадений больше лимита, либо принятое сообщение «ничего не найдено».
Все 10 000 entity достижимы через уточнение поиска; ограничение относится к одновременно созданным DOM-узлам, а не к доступному набору. Выбор закрывает picker, переносит source в draft и возвращает фокус в поле строки. Escape и click/tap снаружи закрывают picker без изменения; Tab не запирается внутри вложенной поверхности и сохраняет нормальный modal focus order. На touch каждая интерактивная строка не меньше 44×44 px.
Active row определяется stable block/value ids, не индексом массива. Если во время draft-операции строка удалена, picker закрывается; reorder не направляет выбор в другую строку. Глобальный поиск над всеми native selects удаляется: поиск живёт рядом с единственным активным списком и не выглядит как фильтр скрытых строк.
6.3 Индекс и обновления
Index строится один раз на состав/пары entity_id + friendly_name текущего
доступного hass.states, а не на каждое state value update и не для каждой
строки. Новая/удалённая/переименованная entity инвалидирует index; обычное
изменение state не сортирует и не перестраивает его. Кеш не переносится между
пользователями или соединениями и очищается при lifecycle reset.
7. Local scales и repeated setConfig
После определения local identity значения icon_scale и font_scale из
houseplan_summary_local_v1:* являются authority для View, kiosk и открытой
формы. setConfig не имеет права перезаписать их старым kiosk key, если runtime
уже загрузил тот же key.
Контракт переходов:
| Событие | Ожидаемое значение |
|---|---|
| Первый запуск, новой записи нет | Валидные legacy icon/font один раз как seed, иначе 100% |
Повторный setConfig, local identity та же |
Текущее фактически применённое local значение без скачка |
| Reload/remount, identity та же | Значение из localStorage этого key |
| Смена user/route/card slot | Значение нового key; ничего из прежнего key не переносится |
| Storage недоступен | Session-only значение действует, показывается принятое предупреждение |
Старый ключ не удаляется и не переписывается. Диапазон 50…300%, шаг 5%, Reset 100% и отсутствие влияния на редакторскую геометрию остаются как в #437.
8. Lifecycle, права и async-операции
Runtime хранит snapshot identity/capabilities: user id (fallback name только по
действующему контракту), route/host/slot, kiosk и право shared edit. На каждом
updated/connect сравнивается snapshot, а не только storage key.
Следующие события закрывают active picker и summary dialog, отбрасывают draft, снимают timer/listener/observer и инвалидируют index/metrics/live-dependency snapshot: реальный route leave, disconnect, reconnect после disconnect, смена user, смена admin/write capability, переход в/из kiosk. Local preferences на диске при этом не удаляются.
Каждая lazy import/save/reload/confirmation операция получает generation token. Поздний результат старой generation может завершить уже отправленную серверную запись, но не может снова открыть dialog, применить старый localShow/local scale, показать сообщение в новой пользовательской сессии или вернуть старые entity dependencies. Новый контекст перечитывает authoritative config и собственные local preferences обычным путём.
Обычная смена размера без disconnect и без смены identity не закрывает draft. Page visibility hidden только останавливает clock по прежнему контракту и сама по себе не считается уходом со страницы.
9. Адаптивная форма
Полная admin-форма и local-only форма используют одну responsive основу. На ширине dialog body, где три колонки не помещаются, каждый размер становится двухстрочным блоком: подпись и процент в первой строке, полноширинный slider во второй; Reset переносится отдельной доступной строкой. На широкой форме допустима существующая компактная компоновка.
Обязательные свойства на 320 и 390 CSS px, light/dark, RU/EN, admin/household/ kiosk и браузерном увеличении текста 200%:
- нет горизонтального overflow у body/footer;
- подписи, проценты 50/100/300%, предупреждения и Save/Cancel/Reset полностью видимы либо достижимы вертикальным scroll;
- footer не перекрывает последний control, safe-area учитывается;
- hit target не меньше 44 px, focus outline не обрезан;
- форма не уменьшает текст для вмещения и не меняет план/overlay layout.
10. Единый контракт обычной записи панели
В backend выделяется единая функция подготовки обычного config candidate. Она
до CONFIG_SCHEMA сохраняет прежний settings.summary_panel при omission, а
после schema выполняет change-aware reference validation относительно exact
stored previous и множества entity, читаемых текущим пользователем.
Функция вызывается одинаково из config/set и plan/optimize. Optimize получает
readable entity ids по той же permission-политике, что config/set; вычисление
не переносится на event loop и остаётся внутри существующего executor/write
lock порядка. Идущие следом wall/marker/opening/junction guards не ослабляются.
Reference validation сохраняет правила #437:
- неизменённая старая broken entity/space разрешена;
- смена только label/title/order не превращает старую ссылку в новую;
- новый/заменённый entity id обязан присутствовать в readable hass states;
- новый/заменённый space scope обязан ссылаться на существующее пространство;
- future version сохраняется lossless и inert, без применения v1 semantics.
11. Writer matrix
| Операция | Authority для settings.summary_panel |
Ссылки и независимые namespaces |
|---|---|---|
config/set |
Candidate; omission сохраняет stored, blocks: [] — явная пустота |
Change-aware v1 refs; остальные settings/unknown fields lossless |
plan/optimize |
Тот же ordinary contract, что config/set |
Те же refs/permissions; Optimize не становится обходом validation |
| Full import/restore | Архив authoritative, включая отсутствие namespace | Structural schema; перенос в другую HA может оставить refs broken; остальные imported namespaces заменяются архивом |
| Space/plan-only import | Exact namespaces текущей установки | Source global settings игнорируются; remap касается только импортируемого space graph |
| Optimize Undo | Exact config из optimize backup | Не переоценивает восстановленную старую ссылку как новый выбор |
| Full Import Undo | Exact config до импорта | Возвращает все прежние namespaces вместе, без preserve-current merge |
| Создание/копирование пространства | Candidate строится поверх exact current config | Панель и все независимые settings сохраняются; copy не переназначает её scope |
| Удаление пространства | Ordinary config candidate | Неизменённый scope удалённого space становится старой broken-ссылкой/warning, а не скрытым сбросом панели |
Для каждой строки тест использует минимум три sentinel namespace: текущий
summary_panel, другое известное setting и неизвестное расширяющее поле. Там,
где операция authoritative, проверяется их полная замена/восстановление; там,
где операция частичная, exact сохранение. Полный импорт остаётся единственным
исключением preserve-on-omission обычной записи.
12. Модель данных и совместимость
Новых persisted-полей нет; settings.summary_panel.version остаётся 1. Store и
plan model version не повышаются. Local key/shape и legacy seed не меняются.
Старый frontend продолжает игнорировать и round-trip-ить namespace через backend preservation. Новый frontend со старым backend сохраняет действующий local-only режим и не отправляет shared write. Future summary version остаётся lossless/inert. Full archive поведение, privacy projection и support package из #437 не меняются.
Нельзя чинить Optimize удалением неизвестных fields, принудительным добавлением derived defaults или превращением отсутствующего namespace в persisted default.
13. i18n
Новые пользовательские строки ограничены active picker: доступное имя поиска,
подсказка уточнить запрос при >100 совпадениях и доступное действие закрытия,
если оно не выражается существующим shell. Они добавляются с полным паритетом
в lazy summary-panel-i18n.ts для RU/EN/DE/FR.
Существующие названия системных показателей, warning broken source, Save/Cancel/Reset, storage warning и проценты не переименовываются. Friendly name, entity id и state values не переводятся House Plan.
14. Производительность
Канонический witness: 3 карточки, 10 блоков × 20 строк (200 rows) в каждой и
10 000 hass.states. Измерение выполняется на одном runner до/после, после
preload lazy editor chunk, не по одиночному ручному Windows timing.
Обязательные границы:
- одновременно не более одного active picker на форму;
- не более 100 entity result rows + 3 system rows + одна current-broken row в DOM одного picker; закрытые 200 строк не содержат entity option-list;
- обычный state-value update даёт 0 index rebuild; hot add/remove/rename — ровно один rebuild на карточку/generation;
- после трёх warmups и не менее 20 samples: открытие уже загруженной формы p95 ≤250 ms, ввод в уже открытом picker p95 ≤50 ms;
- state burst не добавляет long task >50 ms из-за закрытой формы/picker;
- existing initial View gzip budget и lazy boundary #437 не ослабляются.
Если абсолютный timing нестабилен на runner, структурные DOM/rebuild assertions всё равно блокирующие; budget не удаляется, а расследуется/калибруется отдельным решением владельца до S7.
15. Критерии приёмки
AC1 — ограниченный picker сохраняет полный выбор. На fixture 200 rows / 10 000 states каждая читаемая entity находится по friendly name или entity id, системные и текущий broken source доступны, но DOM bounds из §14 соблюдены.
Доказательство: pure search/index unit + browser smoke реальными pointer,
keyboard и touch actions; mutation возврата entities.map() в каждую строку
делает DOM assertion красным.
AC2 — draft и stable row ownership корректны. Выбор меняет только active value по stable ids, reorder не перенаправляет его, delete закрывает picker; Escape/click-out не меняют draft, общий Cancel отбрасывает изменения.
Доказательство: frontend unit + browser smoke; index-based active-row mutation красная.
AC3 — индекс не пересобирается от state value. Обычное обновление значения не сортирует index, hot add/remove/friendly-name change отражаются один раз, user/reconnect не использует прежний index.
Доказательство: instrumented unit/smoke counters; unconditional rebuild mutation красная.
AC4 — local scales переживают repeated same-key setConfig. Значения
200%/150% до и после нескольких setConfig фактически и в форме остаются теми
же; reload/remount читает их из своего key, новая identity не наследует.
Доказательство: runtime unit + two-card/two-user browser smoke; legacy overwrite mutation красная.
AC5 — lifecycle не переносит старый UI state. Route leave, disconnect → reconnect, user/permission/kiosk change закрывают modal/active picker, удаляют draft/timers/listeners и старые dependencies. Late lazy/save/reload completion не меняет новую generation.
Доказательство: fake-timer/promise unit и reuse browser smoke; снятие generation guard краснит late-completion case.
AC6 — mobile/local-only формы доступны. RU/EN, 320/390 px, light/dark, 200% text, admin/household/kiosk показывают доступные роли поля, проценты и действия без горизонтального overflow и с hit targets ≥44 px.
Доказательство: bounding-box smoke + reviewed fresh screenshots/golden; возврат фиксированной трёхколоночной grid красный.
AC7 — config/set и Optimize имеют один ordinary contract. Omission
сохраняет v1/future panel exact; blocks: [] сохраняется как пустая; новые
unreadable entity/missing space отклоняются одинаково; старые broken refs и
независимые settings round-trip-ятся.
Доказательство: pure backend matrix + HA websocket tests обоих endpoints; удаление helper call из Optimize красное через обязательный mutation.
AC8 — import/copy/undo следуют writer matrix. Full import authoritative, space/plan-only import и copy сохраняют target namespaces, оба Undo exact восстанавливают backup, delete space не стирает панель.
Доказательство: backend import/undo harness + frontend space-copy candidate unit; rebuild-settings mutation красная.
AC9 — performance witness проходит. DOM/rebuild bounds и p95/long-task границы §14 выполнены на точном SHA без ослабления других budgets.
Доказательство: новый профиль/артефакт performance harness и checked-in budget; code review фиксирует команду, runner и before/after.
AC10 — совместимость, lazy boundary и docs сохранены. Нет schema/model bump, старый/new frontend/backend и future version ведут себя по §12; initial View не загружает editor/picker; docs описывают фактический контракт.
Доказательство: compatibility/lazy manifest tests, npm run bundle:budget,
node scripts/check-docs.mjs, ревью diff schemas и документации.
16. План реализации и автотестов
- Вынести pure индекс/поиск/лимит active source picker; сначала добавить unit-тесты доступности всех entities и structural DOM bounds.
- Перевести editor на stable-id active picker и удалить global rows filter; добавить клавиатуру/touch/focus smoke.
- Сделать summary runtime владельцем local scales после identity load; связать async callbacks с lifecycle generation и вызвать reset из route lifecycle карточки.
- Перестроить responsive local-size grid; снять bounding boxes и fresh visual evidence по матрице AC6.
- Обобщить backend ordinary summary guard для config/set/Optimize, не меняя порядок остальных validators; добавить readable entity snapshot Optimize.
- Добавить writer-matrix tests для import/undo/copy/delete и sentinels независимых namespaces.
- Добавить performance fixture 200/10k/3, checked-in budget/result schema и diff-aware harness routing; перед beta прогнать полный performance/golden/ smoke по runbook.
Ожидаемые файлы/модули:
src/summary-panel-editor.ts,src/summary-panel-runtime-loaded.ts,src/summary-panel-style.ts,src/summary-panel-host.ts,src/houseplan-card.ts, новый pure picker helper при необходимости;src/summary-panel-i18n.ts(RU/EN/DE/FR);custom_components/houseplan/validation.py,custom_components/houseplan/websocket_api.py, import/export только если matrix test выявит фактическое расхождение;test/summary-panel.test.mjs, browser smoke/performance fixture и budgets,tests_backend/test_summary_panel.py, websocket/import tests;docs/ARCHITECTURE.md,docs/CONFIG-COMPATIBILITY.md,docs/TESTING.md, user guide RU/EN и оба changelog.
17. Риски и меры
- Лимит списка делает entity недостижимой. Лимит только на DOM, поиск идёт по полному index; exact id всегда находится.
- Stale picker пишет не в ту строку. Active owner — stable ids; отсутствие owner закрывает поверхность без mutation.
- Смена user во время save. Generation guard отделяет server outcome от local/UI adoption; новый контекст перечитывает authority.
- Optimize расходится с config/set. Один helper и paired endpoint matrix, а не две похожие последовательности.
- Full restore ошибочно сохраняет текущую панель. Authoritative операции проверяются противоположными sentinels к ordinary omission.
- Мобильный фикс ломает desktop. Responsive breakpoint выбирается по доступной ширине dialog body; wide и narrow golden обязательны.
- Perf-тест ложно зелёный. Структурный count блокирует независимо от timing, timing сравнивается на одном runner и mutation возвращает N×M.
18. Откат
Реализацию можно откатить одним issue-коммитом: persisted schema и local key не меняются, миграции данных нет. Откат возвращает медленный picker, scale/lifecycle и Optimize-пробелы, но не делает сохранённый namespace нечитаемым. Нельзя откатывать только backend guard либо только generation token, оставляя тесты и документацию от нового контракта.
19. Release-артефакты
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.md: быстрый выбор источника, сохранение размеров, mobile/lifecycle и безопасный Optimize;docs/USER-GUIDE.mdи.ru.md: active picker и local settings без новых пользовательских настроек;docs/ARCHITECTURE.md: index/lifecycle generation и единый writer guard;docs/CONFIG-COMPATIBILITY.md: полная writer matrix;docs/TESTING.md: команды, fixtures, DOM/p95 budgets и negative witnesses;- свежие reviewed screenshots/golden wide/narrow light/dark и RU/EN enlarged;
- performance artifact 200 rows/10k states/3 cards на точном SHA;
- security report не требуется: права не расширяются, но permission matrix входит в backend/browser tests;
- перед beta обязательны golden, smoke, performance и Linux CI HA harness по runbook; в цикле реализации — typecheck, unit и build.
20. Принятые технические предположения
Эти пункты не меняют видимый продуктовый контракт и могут быть скорректированы ревьюером при сохранении AC:
- Bounded result limit — 100 entity rows; index может быть per-runtime либо безопасно разделён между карточками по immutable composition signature.
- Active picker реализуется внутри lazy summary editor без зависимости от нестабильных внутренних HA components; конкретный DOM primitive свободен.
- Friendly-name index инвалидируется детерминированной signature, а state values не входят в неё; допускается более дешёвый эквивалентный механизм.
- Lifecycle generation увеличивается на disconnect и identity/capability change; route leave вызывает явный summary reset до warm snapshot.
- Ordinary backend helper располагается в
validation.pyлибо рядом с WS writer; важно единственное исполнение контракта, не имя функции. - Full import missing refs остаются допустимыми transfer broken refs и обнаруживаются формой по правилам #437; новый import-warning UI не вводится.
- Точные имена новых tests/scripts/budget files можно выбрать при реализации; witness, отрицательные мутации и границы AC менять нельзя без нового решения.