Files
houseplan-card/docs/WARM-REMOUNT.md
T
Matysh 1397a71f84
Validate / hacs (push) Failing after 7s
Validate / hassfest (push) Failing after 7s
Validate / frontend (push) Successful in 2m47s
Validate / backend (push) Failing after 8m13s
Validate / smoke (push) Failing after 22m41s
v1.59.0-beta.1: warm remount keeps your view and dialogs, sun ray rim
2026-08-04 18:20:47 +03:00

12 KiB
Raw Blame History

Тёплый ре-маунт: возврат на вкладку без «перезагрузки»

Статус: реализовано (dev, DEV-B703-01…03). Код: src/houseplan-card.ts, модульная памятка warmBoot. Смоки: demo/smoke_warm_remount.mjs, demo/smoke_warm_dialogs.mjs.

1. Что вообще происходит

Lovelace пересоздаёт элемент карточки — выбрасывает старый DOM-элемент и создаёт новый — когда websocket переподключается после долго свёрнутой вкладки, при перестройке дашборда и при переключении видов. Страница при этом никуда не девается, но экземпляр карточки умирает, и вместе с ним умирает всё, что жило в полях экземпляра.

Для владельца это выглядит как «карточка перезагрузилась»:

  • мигал прелоадер (закрыто DEV-B703-01);
  • «чуть-чуть дёргается масштаб» (DEV-B703-03, §2);
  • исчезают открытые диалоговые окна (DEV-B703-03, §3) — это и есть прямое доказательство пересоздания: состояние диалога больше нигде не хранится.

Лекарство одно и то же: модульная памятка warmBoot — обычный Map, который живёт в загруженном JS-модуле, а не в экземпляре. Ключ:

`${window.innerWidth}x${window.innerHeight}|${JSON.stringify(config)}`

Смена размера окна → промах → полный защитный бут (единственный случай, когда хром HA может реально пересобраться заново). Одинаковый конфиг дважды на одной странице неразличим — это задокументированное ограничение (высоты у них всё равно совпадают).

2. Рывок масштаба: фактическая причина

Памятка хранила только hdrH/stageH. Этого мало, потому что:

  1. Пан (_view) не переживал экземпляр вообще. Единственным, что восстанавливалось, был зум — из localStorage (LS_ZOOM, per space). Новый экземпляр приходил с _view = null, updated() → _refitView(), а затем _loadFromServer() звал _restoreZoom(), который центрирует план (_applyView(z, центр vb)). Вид, отъеханный в угол, возвращался в центр. Измерено смоком: до пересоздания view.x=50, y=493, после — x=250, y=293 при zoom 2.2.
  2. Зум редактора не сохраняется намеренно (_saveZoom() выходит при _mode !== 'view': рабочий зум редактора — инструмент, а не «как я хочу смотреть»), а режим редактора сохраняется (LS_NAV). Значит ре-маунт внутри редактора возвращался в тот же редактор, но на зуме ПРОСМОТРА: измерено 3.0 → 1.0.
  3. _viewModeSnap (виджет «куда вернуться при выходе из редактора») умирал вместе с экземпляром — выход из редактора после ре-маунта прыгал второй раз.
  4. _showFar («показать дальние объекты») меняет _baseVb(), то есть саму систему координат, против которой клампится вид.

Правило теперь: памятка хранит не «зум», а весь вьюпорт: пространство, режим, зум, сам прямоугольник _view, _viewModeSnap, _showFar, а также инструмент редактора (_tool, _decorTool), выделение (_selId, _rszSel, _decorSel) и локальный переключатель «показать скрытые» (_showHidden). Восстановление даёт бит-в-бит тот же прямоугольник, а не «тот же зум». _loadFromServer() в этом случае не зовёт _restoreZoom() — центрирующее восстановление осталось только для настоящей навигации (хеш/LS_NAV привели на другое пространство).

Диплинк #space=<id> — явная навигация и по-прежнему сильнее памятки.

Памятка обновляется на каждом updated() (метод _warmSnapshot()), то есть всегда отражает последний ОТРИСОВАННЫЙ кадр. Родиться запись может только из осевшей геометрии (_bootSettled), поэтому холодный бут по-прежнему платит полный защитный цикл.

3. Диалоги переживают пересоздание

В той же записи памятки лежит dlg: { kind, space, mode, data }, где data — живой объект-черновик. Памятка — состояние модуля, её никто не сериализует, поэтому наполовину заполненный диалог устройства вместе с загруженными PDF переезжает бесплатно и без потерь.

Восстановление (_warmReviveDialog) происходит не в setConfig, а на следующий такт после connectedCallback: Lovelace в момент setConfig может ещё держать старый элемент, а живому владельцу диалог красть нельзя.

Условия воскрешения — все обязательны:

  1. запись памятки есть (тёплый ре-маунт, а не холодный старт);
  2. dlg не пуст — то есть в последнем отрисованном кадре прошлого экземпляра диалог был открыт;
  3. прошлый экземпляр отсоединился (freed), и с этого момента прошло не больше WARM_REVIVE_MS (10 c). Перестройка Lovelace укладывается в один такт; ушедший на другой дашборд и вернувшийся через полчаса пользователь не должен встречать диалог, о котором он давно забыл;
  4. то же пространство и тот же режим (d.space/d.mode);
  5. одноразово: dlg съедается в момент попытки восстановления. Третий экземпляр диалог уже не увидит, зомби невозможен.

Осознанное закрытие (Esc / Отмена / Сохранить) чистить памятку отдельно не нужно: следующий же updated() запишет dlg: null. Это сильнее ручной чистки — ни один путь закрытия нельзя забыть.

3.1. Что восстанавливается с черновиком

_spaceDialog (настройки/создание пространства), _markerDialog (устройство), _settingsDialog (общие настройки), _rulesDialog (правила иконок), _openingDialog (проём), _decorTextDialog (надпись), _roomDialog вместе со всей своей обвязкой (_roomEditId, _roomFill, источники температуры и влажности, масштабы подписей, _areaSel/_nameSel, _pendingSplit, _path).

Информационные попапы — _infoCard (карточка устройства) и _openingInfo — восстанавливаются по id, а не по объекту: конфиг мог перезагрузиться под нами, и карточка, отрисованная из устаревшего объекта, была бы враньём. Если объекта с таким id больше нет — попап просто не открывается.

3.2. Что НЕ восстанавливается — и почему

Не воскрешаем Причина
«Выровнять всё по сетке» (_alignDialog) Модалка, всё содержимое которой — «нажмите OK, и я перепишу ваш план». Воскресить подтверждение рядом с человеком, который только что вернулся на вкладку, — прямой путь к слепому клику по разрушающей записи. Открывается одним нажатием из шестерёнки.
Подтверждение объединения комнат (_mergeDialog) Тот же класс: подтверждение необратимой правки геометрии.
Подтверждение действия по тапу (_tapConfirm) Держит замыкание exec на мёртвый экземпляр.
Мастер импорта этажей (_importDialog) updated() сам открывает его, пока конфиг пуст; воскрешение удвоило бы очередь.
Любой диалог с busy: true Запись/загрузка была в полёте. Новый экземпляр не знает, доехала ли она; показать «Сохранить» ещё раз — это приглашение записать дважды. Правду покажет перезагрузка конфига.
Киоск У киоска памятки нет вообще (100dvh, ничего не оседает, редактирование недоступно).

Правило одной строкой: воскрешаем черновик, не воскрешаем решение.

4. Что осознанно отложено

Ре-маунт по-прежнему теряет мелкое промежуточное состояние жестов, у которого нет осмысленного «продолжения» после смены экземпляра: незавершённый контур рисования вне диалога комнаты (_path/_cursorPt), стек отмены ресайза (_rszUndo), выбор разреза (_splitSel, _mergeSel), черновик фигуры декора (_decorDraft), незавершённое перетаскивание подложки (_bdDrag), тост (_toast). Все они живут внутри одного жеста; пересоздание элемента жест и так прерывает.