Files
houseplan-card/docs/CHANGELOG.ru.md
T
Matysh ef270d11b7 v1.46.4: detached plans were being collected as garbage — data loss
Deployed v1.46.3 to my own instance, restarted, and the startup sweep deleted
both floor plans: config/houseplan/plans/ went from f1.svg + f2.png to empty.
The backup is a SecureTar, so they are gone.

The rule was wrong, not the code. v1.46.0 introduced collection that treats
'nothing references this right now' as abandoned and gives it an hour. But
detaching a plan — switching a space to 'draw' — is a normal, reversible action,
and the editor's own comment says the file stays on disk. Those two plans had
been detached for weeks; every pass since v1.46.0 was entitled to remove them,
and the one that finally ran did.

New rule, one for every path:
  * superseded by a commit (was in the old revision, is not in the new) — goes
    immediately; that is the one thing a commit knows for certain;
  * belongs to a space or marker that still exists — never collected, at any
    age, because unreferenced is not abandoned;
  * a per-dialog staging folder (up_*) — one hour, unchanged: by construction it
    only ever holds an upload from a dialog that was never saved;
  * anything else — thirty days.

The  flag I added an hour ago is gone with it: two rules for the same
question is how this happened. Tests updated to the new grace, plus two that pin
the distinction directly.

I am sorry about the files.
2026-07-28 19:55:38 +03:00

59 KiB
Executable File
Raw Blame History

История изменений

Русская версия docs/CHANGELOG.md. Переведены записи начиная с v1.42.0 (2026-07-26); более ранние доступны только в английском файле.

Правило проекта: оба файла пополняются в одном коммите с самим изменением — как и остальная документация (см. docs/STATUS.md).

v1.46.4 — 2026-07-28 (потеря данных: отцеплённые планы убирались как мусор)

  • Отцеплённый план больше не удаляется через час. Переключение пространства в режим «нарисовать» снимает ссылку и, как редактор всегда и говорил, оставляет картинку на диске, чтобы её можно было вернуть. Уборка, добавленная в v1.46.0, этой разницы не делала: считала «на файл сейчас никто не ссылается» синонимом «файл брошен» и применяла часовое правило. На установке автора плановый проход в итоге удалил два плана этажей, отцеплённых несколькими неделями раньше, — восстановить их было нечем. Если вы отцепляли план после v1.46.0 и инстанс перезапускался или проработал сутки — загляните в config/houseplan/plans/ и напишите в Telegram-чат, если файла нет. Правило теперь такое: коммит по-прежнему удаляет ровно то, что заменил, — это он знает наверняка. Всё остальное лишь «непривязано», а это обратимое состояние: файл, принадлежащий существующему пространству или устройству, не удаляется никогда, а всё прочее ждёт тридцать дней вместо часа. Один однозначный случай сохраняет короткое правило: промежуточная папка диалога содержит только загрузку из диалога, который так и не сохранили.

v1.46.3 — 2026-07-28 (перепроверка v1.46.2: HP-1462-01)

  • Уборка при старте действительно убирает. Она искала свои же runtime-данные по домену, а во время запуска Home Assistant ещё не считает интеграцию загруженной — поэтому поиск возвращал пустоту, и проход тихо вырождался в удаление незавершённых передач, оставляя настоящую работу таймеру через 24 часа. При перезапусках чаще, чем раз в сутки, она не выполнялась вообще. Теперь используется объект, который у неё и так был на руках.
  • Тест, который должен был это доказать, проходил по неверной причине. Он создавал лишние файлы до сохранения конфигурации, а сохранение тоже собирает мусор — так что к моменту перезапуска убирать было уже нечего. Переписан: файлы создаются после сохранения; добавлен второй тест, который дёргает плановый таймер отдельно, и третий, который запускает перезапуск и сохранение одновременно и проверяет, что принятая конфигурация никогда не ссылается на удалённый уборкой файл.

v1.46.2 — 2026-07-28 (перепроверка v1.46.1: HP-1461-01, -02)

  • Файл, который в итоге никому не понадобился, убирается, даже если больше ничего не сохраняют (HP-1461-01). Сборка привязана к записи конфигурации — это верно для того, что запись вытесняет, но оставляет зазор: отмените диалог после того, как файл уже загрузился, потеряйте связь сразу после, или вызовите API загрузки напрямую — и на файл никто не ссылается, а будущей записи, которая бы это заметила, нет. Добавленное в v1.46.1 ежедневное подметание убирало только незавершённые передачи, поэтому обещание «отменённое вложение исчезнет через час» не выполнялось там, где никто ничего не правит. Теперь подметание сверяется с сохранённой конфигурацией — под той же блокировкой, что и запись, — и собирает устаревшие непривязанные вложения и планы тоже.
  • Перетаскивание больше не отменяется чужим перемещением (HP-1461-02). Когда в v1.46.1 полная карточка научилась следить за позициями, она защищала те, что вы подвинули, но ещё не отправили, — вот только читала этот список после сброса отложенной записи, а сброс его первым делом опустошает. При настоящем перетаскивании, когда запись уже запланирована, список оказывался пустым, и старая серверная позиция закрашивала ваше движение. Теперь снимок снимается до сброса, и вдобавок удерживаются позиции, отправленные, но ещё не подтверждённые: пока сервер не подтвердил позицию, авторитет по ней — та карточка, которая её подвинула.
  • Два теста доросли до своих же описаний: тест загрузки теперь действительно отменяет задачу запроса, а не только проходит по путям ошибок, а смоук синхронизации позиций планирует настоящую отложенную запись и задерживает её — именно этот порядок и терял перетаскивание.

v1.46.1 — 2026-07-28 (перепроверка v1.46.0: HP-1460-01 … -03)

  • Две загрузки с одинаковым именем больше не сталкиваются (HP-1460-01). v1.46.0 перестала затирать вложения, но выбор свободного имени и его занятие были двумя шагами: две загрузки, попавшие между ними, сходились на одном имени, обе рапортовали успех, и одни байты заменяли другие. Теперь имя занимается атомарно в момент выбора — двадцать одновременных загрузок manual.pdf дают двадцать файлов. Тот же механизм используется при переносе файлов на перепривязке, где был ровно такой же зазор. Заодно там же: имя предельной длины теряло расширение, а суффикс коллизии выталкивал его за предел, и вложение сохранялось под именем, которое сервер обратно не отдаёт — вечный 404 на файл, который интерфейс считал прикреплённым.
  • Прерванная загрузка больше не оставляет временный файл навсегда (HP-1460-02). Уборка стояла в except Exception, мимо которого отменённый запрос проходит насквозь, а сборщик заглядывает только в папки маркеров — поэтому оборванная передача оставляла .upload-*, и убрать его было некому. Теперь убирает любой путь выхода, запрос с двумя файлами отклоняется сразу, а брошенные временные подметаются при старте и раз в сутки. Запись идёт порциями по мегабайту, а не отдельной задачей на каждые 64 КБ.
  • Две полные карточки рядом держат одинаковые позиции (HP-1460-03). v1.46.0 научила следить за перемещениями статическую карточку и оставила позади полную, поэтому перетаскивание иконки в одном окне не двигало её в другом до перезагрузки. Теперь следит — и не мешает собственному перетаскиванию: чужая ревизия, пришедшая в его разгар, сливается, а не накатывается сверху, и карточка не перечитывает то, что записала сама.

v1.46.0 — 2026-07-28 (полный внешний аудит v1.45.4: HP-1454-01 … -10)

Безопасность

  • Загруженный SVG-план больше не является живым документом origin вашего Home Assistant (HP-1454-01, high — блокер релиза). Внутри карточки план подключён через <image>, где скрипты не выполняются; но тот же URL, открытый напрямую, становился документом верхнего уровня в origin самого HA, и <script> в нём мог читать localStorage сессии и обращаться к API. Для загрузки нужно право записи, а оно по умолчанию есть у каждого аутентифицированного пользователя, и подписанную ссылку несложно передать администратору. Ответы с SVG теперь несут заголовок Content-Security-Policy sandbox, который помещает документ в opaque origin. Только SVG — CSP на PDF способен сломать встроенный просмотрщик браузера, а растровая картинка ничего выполнить не может. Для существующих планов ничего не меняется: карточка рисует их ровно как раньше.

Целостность данных

  • Инструкция, приложенная к устройству, больше не затирает предыдущую (HP-1454-02). Загрузка писала прямо в <маркер>/<имя файла>, вне транзакции конфигурации: отмена диалога или отвергнутое сохранение оставляли сохранённую ссылку указывающей на новые байты. А все новые иконки грузили в одну общую папку, поэтому две с файлом manual.pdf начинали ссылаться на один физический файл. Теперь загрузка занимает свободное имя и никогда не перезаписывает, новая иконка получает собственную промежуточную папку, файлы из которой переезжают к настоящей иконке при принятом сохранении, а загрузка, которую никто не сохранил, убирается через час. Имя, на которое уходит коллизия, сменилось с manual (2).pdf на manual-2.pdf: старое санитайзилось на обратном пути, поэтому переименованное вложение записывалось и больше не отдавалось (нашёл новый тест; до этого релиза так же ломались коллизии при перепривязке).
  • Две быстрые правки больше не теряют вторую (HP-1454-03). Debounce разносил старты сохранения, а не сами сохранения. Если одно длилось дольше полусекунды — занятый сервер, медленная связь, — следующая правка уходила с той же ревизией, сервер принимал первую и отклонял вторую, а обработчик конфликта перечитывал серверную копию поверх локальной. Правка исчезала, и сообщение винило «другое окно», которого не было. Записи сериализованы: по одной за раз, каждая с ревизией, которую вернула предыдущая.

Корректность и пределы

  • Открытые границы снова следуют за геометрией (HP-1454-04). Их кэш ключевался только по id комнат и связям, поэтому смена пропорций пространства или перетаскивание вершины оставляли открытые границы — и свет, который через них проходит, — в старых координатах до перезагрузки. Ключом стала сама отрисованная модель, из которой они и вычисляются.
  • Конфигурацию больше нельзя сделать сколь угодно тяжёлой (HP-1454-05). Внешние коллекции были ограничены, вложенные — нет. Полигон на 150 000 точек или список из 100 000 идентификаторов проходили валидацию, а потом каждый рендер по ним ходил. Появились пределы на вершины полигона, связи открытых границ, управляемые сущности, вложения, длину текста и ссылок, плюс общий предел размера сериализованной конфигурации. Устаревшее поле segments отбрасывает сервер, а не надежда на современный клиент.
  • Большие файлы передаются потоком, а не через память (HP-1454-06). Инструкция на 50 МБ целиком читалась в память на приёме и ещё раз на отдаче; пара параллельных скачиваний — заметная нагрузка на слабый хост Home Assistant. Загрузка пишется во временный файл потоком, отдача идёт с диска.

Согласованность

  • Статическая карточка пространства учитывает настройки заливки комнаты (HP-1454-07). Она строит модель другой функцией, и настройки комнаты в неё не переносились — комната, которой вы выключили заливку, всё равно закрашивалась.
  • Перемещение иконки сразу видно на статической карточке (HP-1454-08). Layout — отдельное состояние без ревизии и без события: перетаскивание на полной карточке оставляло соседнюю статическую со старой позицией до изменения конфигурации или перезагрузки страницы. Теперь записи layout хранят ревизию, возвращают её и сообщают о себе — заодно оптимистическая блокировка на полной записи layout начала что-то значить, ведь точечная запись раньше сбрасывала счётчик.
  • Предупреждение о пропавшем плане исчезает вместе с пространством (HP-1454-09). Уборка смотрела только на существующие пространства, поэтому удаление или переименование оставляло предупреждение в «Ремонте» навсегда.
  • Сборка: serialize-javascript поднят выше двух advisory (HP-1454-10). В рантайме недостижим, production-зависимости и так были чисты, но это одна строка.

v1.45.4 — 2026-07-28 (ревью v1.45.3: R5-1, R5-2)

  • Частично успешный ответ на подпись больше не пропускает выдержку (R5-1). Бэкенд подписывает каждый путь независимо: тот, что подписать не удалось, логируется, пропускается, и вызов всё равно завершается успешно с остальными ссылками. Карточка считала любой успешный вызов «весь батч готов», сбрасывала выдержку для всех путей в нём и записывала только вернувшиеся ссылки — и путь, который бэкенд стабильно пропускал, запрашивался заново на каждом рендере, то есть ровно то усиление, ради которого выдержка и вводилась в v1.45.2. Теперь путь считается подписанным, только если в ответе действительно есть ссылка на него; остальные уходят в выдержку по отдельности, ключи, которых не просили, игнорируются, а перерисовка запускается лишь при появлении хотя бы одной новой подписи.
  • Снимок состояния больше не противоречит репозиторию (R5-2). Там всё ещё было написано, что в main лежат релизы только до v1.40.1, и приводились счётчики тестов на несколько релизов назад — при том что строка версии рядом исправно обновлялась. Читающий его человек или агент получал неверную модель веток и заниженное представление о покрытии. Роли веток описаны точно, а счётчики убраны: npm run inventory печатает их из дерева, и устаревать больше нечему.

v1.45.3 — 2026-07-27

  • «Значение вместо иконки» невозможно было сохранить (issue #3). Опция появилась в редакторе устройств ещё в v1.26.0, но серверная схема всё это время принимала только badge, ripple и icon_ripple. При её выборе сохранение падало с not a valid value for dictionary value @ data['config']['markers'][n]['display'], а поскольку один отвергнутый маркер валит всю запись конфигурации, план не сохранялся вообще, пока настройку не отменишь. Спасибо @RemyRoux за отчёт и точный текст ошибки.
  • Списки опций теперь в одном месте и сверяются между языками. DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_MODES и ROOM_FILL_MODES экспортируются из карточки, и backend-тест читает их, проверяя, что схема принимает каждое значение, которое пользователь реально может выбрать. Теперь добавить опцию в редактор и забыть про схему — значит уронить тесты, а не узнать об этом из чужого сообщения об ошибке.

v1.45.2 — 2026-07-27 (закалка по ревью v1.45.1: R4-1, R4-2)

  • Сбой уборки больше не превращает принятое сохранение в ошибку (R4-1). Сборка вытесненных файлов плана идёт уже после того, как конфигурация сохранена, но ошибка при обходе каталога — он может исчезнуть или стать недоступным между проверкой и обходом — вылетала наружу из config/set. Клиент видел неудачу для ревизии, которую сервер закоммитил, а его повтор возвращался с конфликтом. Теперь сборщик сообщает «ничего не убрано» вместо исключения, а config/set пишет в лог и продолжает: событие уходит, новая ревизия возвращается.
  • Один запрос подписи на ссылку вместо одного на рендер (R4-2). Множество ожидающих очищалось в момент отправки батча, а не по возвращении, поэтому пока запрос content/sign был в полёте, каждая перерисовка ставила ещё один — шесть вызовов там, где нужен один, и куда хуже на медленном (а не просто занятом) сокете. Состояния «в очереди» и «в полёте» теперь разделены, после ошибки включается выдержка (2 с с удвоением до 60 с) вместо повтора на следующем кадре, а запрос, который так и не завершился, перестаёт блокировать повторы через 15 с. Поздний ответ, пришедший после размонтирования карточки, больше не вызывает перерисовку.
  • Тесты: восемь юнит-тестов подписывателя с ручным разрешением promise (четыре падают на v1.45.1), backend-тест на то, что сломанный сборщик оставляет сохранение успешным с рабочей ревизией, и проверка исчезнувшего каталога в тестах чистого сборщика.

v1.45.1 — 2026-07-27 (повторное ревью v1.45.0: R3-1, R3-2)

  • Уборка старых файлов плана перенесена внутрь транзакции конфига (R3-1, high). v1.45.0 сделала загрузку безопасной, но отдала удаление клиенту: после успешного сохранения карточка просила бэкенд убрать всё, кроме только что закоммиченного файла. Два открытых редактора невозможно упорядочить — задержавшийся запрос одного клиента удалял план, который только что сохранил другой, и принятая конфигурация оставалась со ссылкой в пустоту, то есть ровно с тем ущербом, ради которого вводился copy-on-write. Команда houseplan/plan/cleanup убрана. Теперь config/set убирает сам, под своей блокировкой, сравнивая конфигурацию, которую заменил, с той, которую принял: файл, на который ссылалась старая ревизия и не ссылается новая, удаляется, а любая другая непривязанная загрузка не трогается, пока ей не исполнится час — свежая может принадлежать ещё не завершённой чужой транзакции.
  • Статическая карточка пространства снова показывает подложку (R3-2). Она подписывала URL и выбрасывала результат — getCardSize() правил временную модель, а render() строил свою заново из конфига, — поэтому <image> запрашивал защищённый путь и на каждом рендере получал 401. Обе карточки теперь используют один подписыватель, и статическая заодно получила батчи, учёт срока годности и периодическое переподписывание, которые были только у основной. Её множество ожидающих запросов освобождается в finally, так что одна неудача больше не блокирует ссылку до конца жизни страницы.
  • Новые тесты: пять backend-сценариев чередования двух клиентов из отчёта (поздний коммит, незакоммиченная загрузка, устаревший сирота, чужие файлы, отвергнутое сохранение), сборщик вынесен в чистый модуль и покрыт юнит- тестами, плюс smoke_space_card_bg на подписанный фон — он падает на v1.45.0, где в DOM попадает сырой URL.

v1.45.0 — 2026-07-27 (внешнее ревью v1.44.8: R2-1, R2-2, R2-3)

  • Отвергнутое сохранение больше не может испортить рабочий план (R2-1, high). Файл плана записывался под финальным именем — попутно удаляя вариант с другим расширением — до проверки ревизии конфига. Если запись потом отвергалась (конфликт ревизий, валидация, обрыв связи), живой план оказывался уже подменён, а сохранённый конфиг мог ссылаться на удалённый файл. Теперь загрузка идёт в версионированное имя (<space>.<токен>.<ext>) и ничего не удаляется; карточка просит бэкенд убрать устаревшие файлы только после принятой записи конфига. Падение между шагами оставляет один осиротевший файл, который подберёт следующая успешная загрузка.
  • Подписанные ссылки больше не протухают навсегда на долгоживущих экранах (R2-2). Бэкенд подписывает не более 200 путей за запрос и молча отбрасывает остальные, а карточка отправляла весь кэш одним вызовом и считала любую запись годной вечно. Начиная с 201-го вложения поздние ссылки переставали обновляться и через 24 часа тихо ломались. Теперь запросы бьются на батчи по общему лимиту, записи помнят свой возраст (стареющая ссылка работает, пока едет замена, протухшая не отдаётся вовсе), а кэш чистится до ссылок, на которые конфиг всё ещё ссылается.
  • Климат комнат считается один раз на обновление, а не на каждую комнату (R2-3). Каждая комната запрашивала температуру и влажность по отдельности, и каждый запрос заново обходил весь реестр сущностей: 60 комнат и 2000 сущностей — это ~120 обходов на рендер, целый кадр на метаданные, которые не менялись. Теперь один проход строит карту по всем зонам с привязкой к снимку Home Assistant: свежие состояния видны всегда, а посторонние перерисовки не стоят ничего. Замер в смоуке: 133 обхода реестра на обновление до, 2 после — и число больше не растёт с числом комнат.
  • smoke_ux_fixes писал скриншот по жёстко зашитому пути /tmp и не запускался на Windows — теперь берёт временную папку у ОС.
  • Новые тесты: smoke_plan_upload_reject (чистка только после принятого сохранения), smoke_sign_cap (201 ссылка, батчи, чистка, срок годности), smoke_climate_once (число обходов не растёт с числом комнат), плюс backend-покрытие версионированных имён и юнит-тесты новых хелперов.

v1.44.8 — 2026-07-27

  • Загруженная подложка действительно привязывается к пространству. _saveSpaceDialog держал ссылку на объект пространства через await загрузки картинки. Любое событие houseplan_config_updated запускает _reloadConfigOnly(), а оно заменяет _serverCfg — ссылка становилась осиротевшей, и plan_url, aspect, заголовок и все настройки отображения писались в отсоединённый объект, тогда как на сервер уходил нетронутый конфиг. Файл попадал на диск, подложка не появлялась, пересохранение не помогало. Создание пространства в этот момент теряло пространство целиком. Теперь загрузка идёт до обращения к конфигу, и ни одна ссылка не живёт через await.
  • _saveConfigNow помечает запись как выполняющуюся (_cfgWriting) — так же, как отложенный писатель, — поэтому чужая ревизия, пришедшая посреди сохранения, откладывает перечитывание вместо подмены конфига (аудит L2, расширен на этот путь).
  • Регрессионный тест demo/smoke_plan_upload_race.mjs падает на v1.44.7 и проходит здесь.

v1.44.7 — 2026-07-27

  • Подложки снова отображаются (регрессия с v1.44.5). Эндпоинт с файлами требует авторизации, поэтому карточка просит бэкенд подписать ссылку на план — но подпись подставлялась внутри мемоизированной модели пространства, а она кэшируется по отпечатку конфига. Неподписанная ссылка «замерзала» в кэше, подпись до элемента <image> не доезжала, и план не грузился никогда. Теперь ссылка вычисляется в момент отрисовки, вне кэша. (Ссылки на PDF не страдали — там она и так вычислялась при отрисовке.)
  • Больше нет «неудачной попытки входа» с собственного IP. Пока подложка была сломана, браузер продолжал дёргать неподписанный путь, тот отвечал 401, и Home Assistant поднимал предупреждение о неудачном входе с адреса самого зрителя. Теперь до получения подписи не рисуется ничего, и неподписанный запрос не уходит вовсе.
  • Долгоживущие экраны не моргают. Переподписывание раз в 12 часов раньше сбрасывало все подписи и ждало новые; теперь текущие ссылки держатся до прихода замены, так что настенный планшет не показывает пустой план.
  • Регрессионный тест demo/smoke_plan_signed.mjs падает на v1.44.6 и проходит здесь.

v1.44.6 — 2026-07-27

  • Климатом комнаты считается только температура воздуха. После v1.44.5, когда данные стали браться из реестра зон, а не с видимых значков, кандидатами стали все скрытые датчики температуры в зоне — в том числе те, что меряют вовсе не воздух. Перед усреднением теперь работают три фильтра: пропускаются сущности с категорией диагностика/настройка, сущности исключённых интеграций и сущности, в id которых назван не-воздушный носитель (water, coolant, flow_temp, return_temp, target, setpoint, chip, cpu, processor, board, device_temp, batter, freezer, fridge, oven, kettle, boiler). На живой установке с 60 зонами это убрало четыре реальных ложных срабатывания: температуру процессора NAS, воду в умном чайнике, сауну с 90 °C и виртуальный better_thermostat, дублирующий настоящий датчик.
  • Новые правила иконок: чайники и термопоты получают mdi:kettle, сауны (sauna, harvia, парная) — mdi:hot-tub. Раньше и те и другие попадали под общее правило термометра, из-за чего и учитывались в климате комнаты.

v1.44.5 — 2026-07-27

  • Климат комнаты считается по всем датчикам зоны, включая устройства, которых нет на плане (скрыты курированием или вами). Раньше среднее бралось только по видимым значкам, поэтому скрытый термометр молча выпадал из карточки комнаты, подсказки и температурной заливки. Курирование сохранено (холодильники, термоголовки и розетки с температурой чипа не считаются), а явно выбранный источник в настройках комнаты по-прежнему главнее.
  • Из подсказки к комнате убрана фраза «открыть зону» — клик по комнате перестал никуда вести ещё в v1.40.1, для перехода есть значок-ссылка у названия.

v1.44.4 — 2026-07-27 (доработка по аудиту: B2, B5, L4)

  • Единая политика авторизации (B2). HTTP-загрузка по-прежнему разрешала запись, когда запись о конфигурации недоступна, тогда как WebSocket-путь уже отказывал — они разошлись. Теперь оба вызывают общий помощник may_write, который в неопределённой ситуации пропускает только администраторов.
  • NaN/Infinity отвергаются во всех координатах (B5). Проверка на конечность числа стояла только у позиций раскладки; прямоугольники комнат, вершины полигонов, view_box и координаты проёмов принимали "NaN", который при записи превращается в null и необратимо портит геометрию. Ограничение MAX_OPENINGS было объявлено, но нигде не использовалось — список проёмов оставался безразмерным.
  • Укрепление перетаскивания (часть L4). Безопасная обёртка над setPointerCapture теперь используется во всех сценариях перетаскивания (устройства, подписи, изменение размера), а не только у проёмов — «мёртвый» идентификатор указателя мог оборвать перетаскивание. Фигуры декора получили ограничение по границам: их больше нельзя утащить далеко за пределы плана и сохранить там.

v1.44.3 — 2026-07-27 (исправление: планы и инструкции снова загружаются)

  • У аутентифицированной выдачи контента не было рабочего пути для браузера. Версия v1.43.0 закрыла дыру правильно, но Home Assistant аутентифицирует HTTP-запросы либо заголовком Bearer, либо подписанным путём authSigа <image href> внутри SVG и обычная ссылка <a href> не отправляют ни того, ни другого. На настоящем дашборде фоны планов и ссылки на PDF отдавали 401 (воспроизведено вживую до исправления). Теперь карточка просит бэкенд подписать то, что собирается показать (houseplan/content/sign, 24 часа, привязано к токену сессии, только для нашего эндпоинта), перерисовывается, когда подписи приходят, и обновляет их каждые 12 часов, чтобы настенные планшеты продолжали работать. Тест бэкенда скачивает подписанный адрес без заголовка авторизации и проверяет 200, а без подписи — 401.

v1.44.2 — 2026-07-27 (внешнее код-ревью: CR-1…CR-3)

Второе, состязательное ревью (версии v1.44.0) дало три находки — все закрыты.

  • Правило про замки теперь сформулировано точно и проверяется (CR-1). Рецензент справедливо отметил, что утверждение «замок нельзя открыть с плана» было слишком абсолютным: кнопка в карточке двери действительно вызывает сервис. Эта кнопка — осознанное продуктовое решение, поэтому правило переписано там, где ему место («никогда случайным нажатием; ровно одна подписанная поверхность»), отпирание теперь спрашивает подтверждение, а новый смок-тест проверяет все пять путей управления и доказывает, что значки, controls[] и карточка устройства по-прежнему отказывают замкам.
  • Перенос вложений стал транзакционным (CR-2). При смене привязки маркера файлы раньше ПЕРЕМЕЩАЛИСЬ до сохранения конфига с проверкой ревизии: если сохранение отклонялось, на сервере оставались старые ссылки, а файлы уже уехали. Теперь сервер копирует, конфиг фиксируется, и только после этого старая папка удаляется (houseplan/files/cleanup).
  • Неудачный или частичный перенос больше не переписывает ссылки (CR-3). Копирование возвращает точное соответствие «исходное имя → записанное»; переписываются только подтверждённые копии, при совпадении имён файл получает уникальное имя вместо молчаливой ссылки на чужой файл, а ошибка переноса показывается тостом.

v1.44.1 — 2026-07-27

  • Ссылка на чат сообщества добавлена везде, где её ищут: https://t.me/ha_houseplan (бейдж и строка в шапке обоих README, раздел «Помощь и обмен опытом», контакт-ссылки в шаблонах issue, CONTRIBUTING, STATUS и SCOPE).

v1.44.0 — 2026-07-27 (отзыв пользователя: сначала управление)

  • Карточка устройства стала поверхностью управления. Она открывается со списка управляемых сущностей: лампы, розетки и вентиляторы переключаются прямо здесь кнопками под палец, шторы, замки и климат передают управление в штатный more-info Home Assistant. Модель, ссылки и PDF-инструкции ушли ниже — на настенном планшете эта карточка нужна для управления домом, а не для чтения документации (из полевого отзыва). Служебные (config/diagnostic) сущности в списке не показываются, замки по-прежнему не переключаются нажатием в карточке.
  • «Это устройство — источник света» — новый флаг у устройства. Умный выключатель с обычными (не умными) светильниками теперь даёт ореол в заливке «Свет по источникам» без создания хелпера-группы: свечение следует за самим выключателем либо за лампами, привязанными в «Управляет источниками света».

v1.43.3 — 2026-07-27 (отзыв пользователя: обнаруживаемость и тач)

  • Настройки комнаты невозможно было найти. Шестерёнка из v1.42.0 жила внутри подписи комнаты размером 0.9em от её шрифта и с прозрачностью 60% — несколько бледных пикселей на обычном плане. Теперь это кнопка-пилюля «⚙ Комната» фиксированного читаемого размера, не зависящая от масштаба карточки, и она появляется даже у комнат без имени (их там и называют). Заодно разблокировались слайдеры размеров шрифта, до которых никто не мог добраться.
  • Строка показателей увеличена с 0.62 до 0.75 от размера названия — автор отзыва мог увеличить название, но строка датчиков оставалась нечитаемой на планшете. Множители комнаты и пространства работают поверх.
  • Тултипы на тач-устройствах, вторая попытка. Проверки (hover: none) оказалось мало: некоторые устройства, оболочки, стилусы и подключённые мыши сообщают hover: hover, и подсказки продолжали висеть под пальцем. Теперь карточка запоминает первое же касание (touch/pen) и гасит открытую подсказку.

v1.43.2 — 2026-07-27 (внешний аудит: слой тестов)

  • Смок-тесты наконец умеют падать (T1). Все 48 браузерных смоков печатали булевы значения и всегда завершались с кодом 0 — регрессия была видна в их собственном выводе, а прогон считался успешным. demo/serve.mjs теперь экспортирует check/checkAll/finish: каждый факт проверяется по имени, несовпадения и необработанные исключения внутри карточки дают ненулевой код возврата. Проверено намеренной поломкой блокировки редакторов в киоске — соответствующий смок покраснел.
  • Набор гоняется в CI (T2) отдельной джобой smoke после frontend, против свежесобранного бандла (закоммиченная копия в demo/srv/assets — снимок, на нём легко получить «зелёный» отчёт о несуществующем коде), с выгрузкой логов при падении.
  • docs/TESTING.md приведён в соответствие (T3). [auto] теперь означает «существует именованная проверка, которая падает», и рядом написано, где она; 72 пункта, где автоматизация была намерением, честно помечены [manual]. Исправлены два давних противоречия: строка про «ноль кнопок редактирования в Просмотре» (неверна с v1.30.1) и строка про клик по проёму (снова верна с v1.43.1).
  • Три смока проверяли поведение, которого уже нет (ожидания времён v1.39.0 и v1.25); теперь они тестируют текущий контракт.

v1.43.1 — 2026-07-27 (внешний аудит: исправления P1)

  • Стоимость отрисовки (L1). Home Assistant подменяет объект hass при любом изменении состояния в доме, и каждая такая отрисовка пересчитывала всю геометрию плана — _openPairs() вызывался по разу на комнату (кубическая математика коллинеарных наложений), модель пространства строилась дважды. Теперь и то, и другое мемоизируется по структурному отпечатку конфига и вынесено из цикла по комнатам; сброс кэша происходит синхронно в момент мутации, а не внутри дебаунса.
  • Тап против перетаскивания у проёмов (L4). У перетаскивания двери или окна не было порога движения, поэтому любое дрожание пальца считалось перетаскиванием: диалог свойств не открывался, а в конфиг писалось неизменённое состояние (что подпитывало гонку L2). Теперь порог 3 px, как во всех остальных сценариях перетаскивания, и запись только при реальном изменении геометрии.
  • Вогнутые комнаты (G2). Вложенность определялась через среднее арифметическое вершин — а оно лежит СНАРУЖИ U- и L-образных комнат, поэтому комнаты-острова в них отвергались как пересечение, а дырка в заливке не рисовалась. Теперь вычисляется настоящая внутренняя точка (interiorPoint).
  • Дедупликация стен (G3). segKey сортировал концы по сырым float, а печатал округлённые, поэтому одна общая стена могла дать два ключа и рисовалась дважды. Сначала округление, потом сортировка.
  • Укрепление бэкенда (B2–B5). Проверка прав на запись теперь отказывает, когда запись о конфигурации недоступна (раньше во время перезагрузки интеграции запись разрешалась); layout/set поддерживает expected_rev и сообщает о конфликте так же, как хранилище конфига; config/set без expected_rev поверх непустого хранилища пишет предупреждение в лог; координаты отвергают NaN/Infinity, а у пространств, комнат, маркеров, декора и раскладки появились щедрые ограничения размера.

v1.43.0 — 2026-07-27 (внешний аудит: исправления P0)

Внешний аудит кода версии v1.41.1 нашёл четыре критические проблемы. Все четыре исправлены и покрыты регрессионными тестами.

  • Молчаливая потеря правок при сохранении (L2). Отложенная запись конфига читала его в момент срабатывания, поэтому пришедшее в промежутке событие houseplan_config_updated подменяло конфиг, и правка пользователя исчезала без единой ошибки — воспроизводилось даже в одной вкладке. Теперь дебаунс умеет flush()/pending(), перезагрузка сперва дописывает отложенную запись и откладывается, пока запись в полёте, а неудачная перезагрузка наконец сообщает о себе вместо молчания.
  • Разрез разрушал геометрию комнаты (G1). Разрез, начинающийся и заканчивающийся на ОДНОЙ стене (вырезание ниши — совершенно естественное действие), давал две самопересекающиеся комнаты, суммарная площадь которых вдвое превышала исходную, и проверка пересечений это не ловила. Теперь такие разрезы корректно вырезают нишу, а инвариант разбиения (части в сумме дают исходную площадь) отклоняет всё остальное.
  • Планы и загруженные файлы отдавались без авторизации (B1). Любой, кто мог достучаться до вашего Home Assistant, скачивал планы этажей и вложенные инструкции без входа в систему. Теперь их отдаёт аутентифицированный обработчик; сохранённые старые адреса переписываются на чтении, так что ничего не ломается. Старые публичные пути исчезают только после перезапуска Home Assistant.
  • Диалоги могли воскреснуть и обнулить карточку (L3). Закрытие диалога во время неудачного сохранения превращало его состояние в пустую «оболочку», отрисовщик падал, и карточка оставалась пустой до перезагрузки страницы. Защита добавлена во все четыре процедуры сохранения, тост об ошибке по-прежнему показывается.

v1.42.2 — 2026-07-26

  • На тач-устройствах подсказки при наведении больше не выскакивают при каждом касании (из отзыва: «на планшете при тапе вылезают доп. надписи — мешают»). Подсказки теперь только для мыши; на тач та же информация есть в карточках комнат и в карточке устройства по долгому нажатию.

v1.42.1 — 2026-07-26 (размеры шрифтов карточек комнат)

  • Закрываем отзыв «нельзя настроить размер шрифта»: три слайдера. В настройках пространства появился базовый размер шрифта карточек комнат для всего пространства; в настройках комнаты — независимые размеры названия и строки показателей (50–300% каждый). Эффекты перемножаются и складываются с растягиванием карточки за уголки и множителем экрана в киоск-режиме.
  • В обоих диалогах показывается живой пример карточки, который меняется прямо во время перетаскивания слайдеров.

v1.42.0 — 2026-07-26 (настройки комнаты — третий уровень)

  • У настроек теперь четыре уровня: общие → пространство → комната → устройство; более конкретный уровень переопределяет более общий (решение владельца, зафиксировано в ARCHITECTURE). В этом релизе добавлен уровень КОМНАТЫ.
  • У каждой карточки комнаты в редакторе плана появилась шестерёнка: переименовать комнату, сменить её зону HA, переопределить тип заливки только для этой комнаты (работает и в glow-пространствах — «без заливки» выводит комнату из темноты) и выбрать явный источник температуры и влажности — любое устройство или сущность HA вместо среднего по комнате. Источник питает карточку комнаты, всплывающую подсказку и температурную заливку и работает даже у комнат без зоны HA (случай из отзыва: собственный template-сенсор, привязанный к помещению).
  • Тот же раздел настроек появляется в диалоге комнаты сразу после замыкания контура.