29 KiB
Issue #131 — полный первый кадр View у read-only-пользователя
- Issue: https://github.com/Matysh/houseplan-card/issues/131
- Редакция: первая редакция для независимого ревью; статус задачи определяется только метками issue
- Тип / приоритет: bug / P1
- Оценка: пользовательская ценность 9/10; ценность для разработки 7/10; сложность и риск 4/10
- Область: холодная загрузка
houseplan-card, выбор пространства, read-only View/киоск, локальный config-cache, reload и warm remount - Модель данных: без изменений и миграции
- Связано: #73, #93,
docs/SCOPE.md,docs/TOUCH-SUPPORT.md,docs/UX-MODES.md,docs/USER-GUIDE.ru.md,docs/ARCHITECTURE.md
1. Сценарий и продуктовый контекст
Персона: домочадец без права редактирования плана либо пользователь настенной панели/киоска. Для него View — основной продуктовый режим, а не предпросмотр перед редактором.
Поверхность и момент: пользователь впервые открывает Lovelace-страницу после чистой установки, очистки локального состояния или на новом браузере. Сервер разрешает прочитать House Plan, но HA-сессия не разрешает одну или несколько подписок на служебные события интеграции.
Задача поддерживает:
- J1: план должен быть читаемым и полным сразу при открытии;
- J6: reload, reconnect и техническое перемонтирование не должны менять смысл или состав видимого плана;
- гарантированный View/touch/kiosk-контракт: телефон, планшет и настенная панель являются целевыми устройствами просмотра и управления.
2. Что человек увидит до и после
До: при первом открытии видна только часть плана, ни одно пространство не выделено, а нажатие его вкладки неожиданно «дорисовывает» пол, мебель, стены, свет и подписи состояний.
После: первый кадр уже совпадает с нормальным видом после нажатия вкладки; пространство выбрано, все его слои и состояния на месте, а нажатие активной вкладки ничего не исправляет.
3. Проблема и подтверждённая причина
setConfig() начинает без server-cache с legacy-значения _space = 'f1'.
После ответов houseplan/config/get и houseplan/layout/get метод
_loadFromServer() принимает конфигурацию через _adoptStructuralResponses(),
но до выбора реального пространства последовательно ожидает три live-sync
подписки.
Отказ первой subscribeEvents() попадает во внешний catch и пропускает весь
оставшийся хвост успешной инициализации:
- hash/saved/default/first precedence не применяется;
_cacheSnapshot()не вызывается;- zoom/viewport не восстанавливается по принятому пространству;
- ошибка ошибочно считается сбоем всей загрузки и запускает полный retry.
При этом _loadOk уже установлен и server config уже принят. _spaceModel()
скрывает часть ошибки, подставляя первый model space вместо несуществующего
f1, поэтому устройства и часть геометрии видны. Но _curSpaceCfg ищет точное
совпадение и остаётся undefined; header также сравнивает точные ID. Из-за этого
неактивна вкладка и отсутствуют зависящие от raw-space слои.
Диагностика на dev SHA 0e69c4a18337 с двумя пространствами home и
upstairs:
| Сценарий | _space |
model fallback | exact space | active tab |
|---|---|---|---|---|
| Cold start, подписка запрещена | f1 |
home |
— | — |
После клика home |
home |
home |
home |
home |
| Технический remount без config-cache | f1 |
home |
— | — |
Новый экземпляр с валидным LS_NAV, без config-cache |
f1 |
home |
— | — |
| Reload с валидным config-cache | home |
home |
home |
home |
can_write: false без отказа подписок загружается правильно. Значит, условие
дефекта — не запрет редактирования сам по себе, а ошибка необязательной подписки
в обязательной последовательности cold start.
4. Решения владельца
Владелец принял defaults D1–D3 14.08.2026. Каноническая запись: https://github.com/Matysh/houseplan-card/issues/131#issuecomment-5287280361
- Задача имеет приоритет P1 и идёт полным маршрутом без
small/trivial. - После принятия серверной конфигурации карточка выбирает ровно одно существующее пространство по действующему приоритету до пространственного рендера.
- Полнота initial snapshot не зависит от права записи, результата необязательных live-sync-подписок, числа пространств, наличия cache, reload или технического remount. Отказ подписки может отключить только последующую live-синхронизацию.
5. Скоуп
В задачу входят:
- обязательное завершение server snapshot после успешных
config/getиlayout/get, даже если любая houseplan event subscription отклонена; - выбор валидного пространства до первого пространственного кадра;
- cold start без
LS_CFG, включая валидный и staleLS_NAV; - одно и несколько пространств, валидные и stale
#space/default_floor; - сохранение принятого server snapshot в
LS_CFGпосле успешной загрузки; - reload и same-route warm remount;
- View и киоск при
can_write: false; обычная admin-сессия остаётся той же; - независимая best-effort установка config, trail и layout subscriptions;
- отсутствие полного load-retry только из-за отказа необязательной подписки;
- unit/browser regression coverage, архитектурная документация и два changelog.
6. Не входит в задачу
- выдача read-only-пользователю права редактировать план или подписываться на запрещённые HA events;
- изменение backend permissions, websocket API или Home Assistant auth;
- гарантированная live-синхронизация после явно запрещённой подписки;
- новый warning, toast, recovery overlay или индикатор ограниченных прав;
- изменение порядка вкладок, названий пространств,
default_floor, deep link или формата локального cache/navigation; - изменение выбора пространства в
houseplan-space-card, где пространство задаётся отдельным обязательным параметром; - изменение рендера пола, мебели, стен, Glow, устройств либо их состояний после того, как raw space уже выбран правильно;
- изменение reconnect/asset-failure контракта #73 для обязательных данных;
- schema/config migration, import/export и новые compatibility-поля;
- отдельная оптимизация производительности или переработка всего boot lifecycle.
7. Контракт поведения
7.1. Инвариант выбранного пространства
Если принятая серверная конфигурация содержит хотя бы одно пространство, перед публикацией spatial candidate одновременно выполняются условия:
_spaceравен ID существующего пространства;- model space и exact raw space описывают один и тот же ID;
- ровно одна вкладка full card имеет active-состояние, кроме киоска, где header намеренно не рендерится;
- все raw-space consumers получают один и тот же объект пространства;
- несуществующее legacy/stale значение не остаётся скрытым за model fallback.
Если пространств нет, карточка сохраняет существующий empty/onboarding-контракт: она не обязана выдумывать ID и не падает.
7.2. Приоритет выбора
Для cold/reload без уже принятого валидного same-route warm viewport применяется существующий порядок, причём каждый кандидат обязан присутствовать в live model:
- валидный
#space=<id>; - валидное сохранённое пространство
LS_NAV; - валидный
default_floorкарточки; - первое пространство live config.
Невалидный кандидат пропускается, а не сохраняется как _space. Explicit hash
по-прежнему выигрывает. Уже принятый валидный same-route warm viewport остаётся
существующим continuity-исключением #73/#93 и не сбрасывается менее точным
saved/default значением; при отсутствии его пространства в live config порядок
выше применяется заново.
7.3. Обязательная и необязательная части загрузки
Успешный initial snapshot состоит из обязательных шагов:
- получить config и layout;
- при структурном изменении подготовить обязательный backdrop;
- принять config/layout, revisions и
can_write; - выбрать валидное пространство;
- записать локальный snapshot;
- восстановить применимый viewport/zoom;
- опубликовать полный candidate frame и построить устройства.
Config/trail/layout event subscriptions являются best-effort live-sync. Ни одна из них не может отменить или задержать перечисленные гарантии после принятия server data.
7.4. Деградация live-sync
- Каждая подписка устанавливается независимо; отказ одной не запрещает попытки установить остальные.
- Успешная подписка сохраняет текущий idempotent one-subscription-per-card контракт и штатно очищается при disconnect.
- Неуспешная подписка остаётся доступной для повторной попытки при следующем обычном load/reconnect, но не запускает tight loop и не перечитывает весь snapshot только ради подписки.
- Отказ подписки не показывает новый toast и не подменяет принятый config fallback-данными.
- Ошибка обязательных config/layout calls, обязательного asset или последующей config reload сохраняет нынешний stale-while-revalidate/recovery-контракт и не маскируется как успех.
7.5. Reload, cache и warm remount
- После успешного server snapshot
LS_CFGзаписывается независимо от результата подписок. - Сохранённый
LS_NAVприменяется после появления live model, даже если до server response не было config-cache, в котором можно было проверить ID. - Технический same-route remount сохраняет валидное пространство по текущему
warm/navigation contract; отсутствие cache не возвращает
_spaceкf1. - Валидный старый cache может дать мгновенный полный кадр, но не является условием корректности.
- После исправления клик уже активной вкладки является no-op и не меняет состав сцены.
8. Архитектурный контракт реализации
Конкретные helper names являются техническим выбором автора, но обязательны следующие границы:
- Нормализация пространства является частью принятия structural snapshot и
выполняется до первого
await, который относится только к live-sync. - Один resolver владеет проверкой существования и cold precedence; setConfig, live load и warm path не должны получать расходящиеся копии правил.
- Model fallback не считается доказательством валидного exact space. После принятия непустого config invariant проверяется по точному ID.
- Подписки изолируют rejection по отдельности; rejected Promise не достигает outer catch обязательной загрузки и не создаёт unhandled rejection.
- Ошибка optional subscription не устанавливает recovery/error state полного
кадра и не запускает
_scheduleLoadRetry(true)сама по себе. - Существующие
_unsubCfg,_unsubTrail,_unsubLayoutостаются authority идемпотентности: успешную подписку нельзя дублировать. - Cache записывает только уже принятые server config/layout и revisions; optional subscription state в persistent data не добавляется.
- Device rebuild и continuity candidate читают уже нормализованное пространство; отдельной исправленной ветки рендера для read-only быть не должно.
Предполагаемые файлы реализации:
src/houseplan-card.ts;- при полезном выделении pure resolver — небольшой frontend module;
- соответствующий
test/*.test.mjs; demo/smoke_readonly_cold_start.mjsлибо эквивалентный browser scenario;docs/ARCHITECTURE.md,docs/CHANGELOG.md,docs/CHANGELOG.ru.md.
9. Модель данных, compatibility и миграция
Форматы CardConfig, server config, layout, LS_CFG, LS_NAV и warm memo не
меняются. Новых полей, schema version, backend validation и compatibility aliases
нет.
Сохранённые планы не переписываются. Исправление вычисляется на каждом старте; конфигурации прежней версии остаются читаемыми новой и старой версиями. Прямая и обратная миграция не нужны.
10. UX, i18n, accessibility и touch
Новых controls, текстов, фокуса, клавиатурных команд или motion нет. Новые i18n-ключи en/ru не требуются.
Активная вкладка сохраняет существующую семантику кнопки и focus order. Исправление не должно программно переводить фокус и не должно объявлять служебную ошибку подписки через live region.
View на touch блокирующий: первый кадр телефона/планшета обязан быть полным без тапа по вкладке. В киоске требование ещё строже, потому что header скрыт и обход недоступен. Touch editor остаётся вне скоупа; право редактирования не меняется.
11. Критерии приёмки
- AC1 (
unit): resolver cold selection покрывает матрицу valid/stale hash,LS_NAV,default_floor, legacy current ID, одного/нескольких/нулевого числа пространств и всегда возвращает существующий ID при непустом model. - AC2 (
unit+ревью кода): после принятия config/layout exact space нормализован и cache/viewport finalization выполнены до optional subscription; rejection любой подписки не может пропустить эти шаги. - AC3 (
smoke): read-only cold start безLS_CFG, с пространствамиhome/upstairs,can_write: falseи rejected houseplan subscriptions сразу имеет_space = home, exact raw spacehome, одну active-вкладку и полный набор floor/decor/wall/Glow/device-state слоёв без клика и page error. - AC4 (
smoke): fixed HA snapshot до клика активной вкладки и после её no-op клика имеет одинаковые проверяемые spatial layers; сцена не «дорисовывается». - AC5 (
unit+smoke): valid/stale saved/default/hash precedence на нескольких пространствах выбирает ожидаемый live ID; stalef1никогда не остаётся exact selection, если такого пространства нет. - AC6 (
smoke): после cold start с rejected subscriptionsLS_CFGсуществует; reload и same-route warm remount без предварительного admin-cache сохраняют полный кадр и валидное пространство. - AC7 (
unit+ревью кода): rejection config, trail или layout subscription изолирован: остальные подписки всё равно предпринимаются, успешные не дублируются, unhandled rejection и full-load retry storm нет. - AC8 (
smoke): тот же cold сценарий сkiosk: trueрендерит полный план при скрытом header; никакого пользовательского действия для восстановления не требуется. - AC9 (
unit+ревью кода): обязательныйconfig/get,layout/getили asset failure продолжает действующий stale-while-revalidate/recovery путь; optional и mandatory errors не смешиваются. - AC10 (
ревью кода): исправление не выдаёт право записи, не вызывает write/service API, не показывает редакторы приcan_write: falseи не меняет backend/security boundary. - AC11 (
unit+ревью кода): на успешную карточку остаётся не более одной подписки каждого типа; нормализация не добавляется в HA state render hot path. - AC12 (
build+ревью кода): оба changelog и архитектурный документ описывают исправление, пользовательское руководство остаётся правдивым без нового контракта, а три bundle snapshot после build побайтно совпадают.
12. План автотестов и проверок
Unit
- Выделить или напрямую покрыть pure resolution matrix из AC1/AC5.
- Покрыть mandatory/optional orchestration контролируемыми fulfilled/rejected Promises: порядок finalization, независимость трёх подписок, idempotency и отсутствие retry от optional failure.
- Сохранить регрессию обязательного fetch/asset failure из AC9.
Browser smoke
Новый узкий сценарий создаёт отдельную full card с пустым House Plan
localStorage, non-admin hass, can_write: false, двумя пространствами с ID, не
равными f1, и управляемыми subscription outcomes. Он проверяет AC3/AC4/AC6/AC8
по внутреннему exact state и реальному shadow DOM, а не только по _model
fallback.
По действующему решению владельца в цикле реализации запускаются только
typecheck, unit и build. Smoke добавляется вместе с кодом, а его штатный
прогон входит в pre-beta browser-smoke gate; независимый код-ревьюер вправе
выполнить целевой сценарий для проверки AC.
Golden и ручные изображения
Нового намеренного визуала нет: правильный результат уже совпадает с состоянием после клика и существующими View/golden. Новые baseline и их принятие не нужны. Скриншоты до/после уже приложены к issue и используются как диагностическая ссылка, не как новый эталон.
13. Производительность и security
Изменение выполняется один раз на structural load, а не на HA state tick. Дополнительного boolean geometry, DOM-слоя, polling или per-frame resolver нет. Количество успешных подписок не растёт. Performance benchmark и новый budget не нужны; штатный performance gate остаётся pre-beta проверкой.
Security boundary не ослабляется: read-only-пользователь только использует уже разрешённые read calls. Запрещённая подписка не эмулируется, backend permission не обходится, write API не вызывается. Отдельный security artifact не нужен.
14. Риски
- Регрессия precedence: повторная нормализация может затереть valid hash или same-route warm viewport. Закрывается AC1/AC5/AC6.
- Дубликаты подписок: независимый retry может создать два listener после
позднего успеха. Закрывается authority через
_unsub*и AC7/AC11. - Ложный успех mandatory load: слишком широкий
catchможет скрыть отказ config/layout/asset. Закрывается явной границей phases и AC9. - Partial live-sync: read-only-сессия может не получать последующие внешние изменения. Это допустимая деградация запрещённой подписки, но initial snapshot обязан оставаться полным.
- Cache leakage между browser tests: сценарий должен изолировать и очищать House Plan keys, иначе валидный admin-cache маскирует регрессию.
15. Release-артефакты
Поскольку исправление пользовательски видимо, реализационный коммит содержит:
docs/CHANGELOG.md— EN bug-fix bulletin со ссылкой на #131;docs/CHANGELOG.ru.md— эквивалентный RU bulletin со ссылкой на #131;docs/ARCHITECTURE.md— граница mandatory initial snapshot и best-effort live-sync subscriptions, плюс invariant exact space;- user guide — без изменения: он уже обещает правильный first space, read-only View и полноценный kiosk/touch; задача приводит код к этому тексту;
- новый unit и browser regression scenario;
- build-синхронизация
dist/houseplan-card.js,custom_components/houseplan/frontend/houseplan-card.jsиdemo/srv/assets/houseplan-card.js.
Golden baseline, screenshots, migration, backend, performance и security артефакты не создаются по причинам из разделов 9, 12 и 13. Перед бетой идут штатные golden verify, полный browser smoke и performance gate по release runbook.
16. Откат
Откат — обычный revert frontend-изменения и синхронных документации/changelog/ тестов/bundle snapshot. Данные и localStorage форматы не меняются, поэтому чистить cache или восстанавливать конфигурацию не требуется.
После отката у затронутой read-only-сессии вернётся прежний неполный cold frame; пользовательским временным обходом остаётся выбор пространства там, где header доступен. Feature flag и обратная миграция не нужны.
17. Принятые технические предположения — можно менять без пересмотра продукта
- Предпочтительно выделить один pure resolver cold precedence, но точное имя и файл не являются продуктовым решением.
- Подписки можно устанавливать после mandatory finalization последовательно
с локальными
try/catchили общей best-effort orchestration; наблюдаемый контракт и AC важнее формы. - Повторная попытка rejected subscription использует следующий штатный load/reconnect, без отдельного polling timer.
- Отказ optional subscription остаётся тихим: новый UX ограниченных live updates потребовал бы отдельного продуктового решения.
- Browser smoke может расширить существующий WS/warm lifecycle scenario вместо нового файла, если сохраняет изоляцию cache и все проверки AC.
- Valid same-route warm viewport трактуется как уже принятое пространство, а не как новый пятый cold persistence source; explicit valid hash сохраняет действующий приоритет.