Compare commits

..
Author SHA1 Message Date
claude[bot] ef0efd5855 docs: review document for #434
Issue: #434
User-Visible: no
2026-09-03 11:30:47 +00:00
Sergey Matyunin c706f8a744 fix: remove stale decor catalog import
Issue: #434
User-Visible: no
2026-09-03 14:25:33 +03:00
claude[bot] bdba9f48ff docs: review document for #434
Issue: #434
User-Visible: no
2026-09-03 11:21:22 +00:00
Sergey Matyuninandclaude[bot] dbda64e89d test: catch stale danger locale gate
Issue: #434
User-Visible: no
2026-09-03 10:55:16 +00:00
Sergey Matyuninandclaude[bot] cba13621c8 fix: harden v1.71 audit boundaries
Issue: #434
User-Visible: yes
2026-09-03 10:55:16 +00:00
claude[bot] 4190db54ab docs: review document for #434
Issue: #434
User-Visible: no
2026-09-03 10:55:16 +00:00
Sergey Matyuninandclaude[bot] a2cdd360bc docs: bound smoke job in audit polish spec
Issue: #434
User-Visible: no
2026-09-03 10:55:16 +00:00
claude[bot] f383d8fd87 docs: review document for #434
Issue: #434
User-Visible: no
2026-09-03 10:55:15 +00:00
Sergey Matyuninandclaude[bot] 8a75c53eca docs: specify v1.71 audit polish
Issue: #434
User-Visible: no
2026-09-03 10:55:15 +00:00
Claude d1091b6d47 test(decor): путь загрузки картинки под автотестом
`src/backdrop-pick.ts` и `src/decor-image-editor.ts` не входили в
tsconfig.test.json — юнитов у них не было вовсе. Смок при этом подменял
`hass.callWS` целиком и до `uploadFromInput`, `upload`, `delete` не
доходил; в шапке файла это было честно написано. AC1 #51 обещал
доказательство «component unit + smoke» — не было ни того, ни другого.

Цена известна: #427 — файл тяжелее 2 МиБ нельзя было добавить ничем,
гасли обе кнопки диалога, включая «уменьшить копию», — прожил четыре
круга ревью именно потому, что этот путь не проверяло ничто.

Оба файла теперь в tsconfig.test.json (компиляция +6 с, без правок
продуктового кода) и покрыты 13 юнитами:

- классификация: проходной файл, тяжелее лимита источника (guard с
  probe.kind='safe' — предупреждение о размере, не об отказе), warn,
  hard, unknown, неподдерживаемый формат;
- состав кнопок диалога на трёх случаях. Декор свыше лимита: «Отмена» и
  «уменьшить копию», без «оставить оригинал» — это и есть #427. Подложка
  на том же файле сохраняет обе кнопки, hard остаётся с одной «Отмена»,
  unknown предупреждает, а не проходит тихо;
- uploadFromInput: сброс input.value, молчание на занятом, отказ формата,
  исключение классификации, передача флага замены в оба слота;
- upload: multipart-поле и URL, порядок каталога, палитра против замены
  ссылки в диалоге, восемь ответов бэкенда с разными текстами, отпускание
  busy на отказе, запрет второго запроса поверх незакончившегося;
- delete: ссылки на ассет, отказ в подтверждении, чистка каталога, карты
  и палитры, различение in_use, сохранность каталога на отказе сервера;
- loadCatalog: отбор валидных строк и тост на отказе.

Смок `smoke_decor_images.mjs` получил последний блок, который проходит
настоящие uploadFromInput → upload → delete в собранном бандле, подменяя
только транспорт: fetchWithAuth и одну ветку callWS.

Свидетели (§2.7, #435). Девять мутаций прогнаны на собранном дереве,
каждая краснит свой тест: условие #427 → тест состава кнобок; снятие
лимита источника; input.value; флаг замены; too_large; busy в upload;
подтверждение удаления; used_by; проглоченный отказ каталога. Мутация
busy сначала вешала прогон и уносила пять тестов cancelledByParent —
тест переписан так, чтобы падать на сравнении, а не по таймауту.

В реестр добавлены три мутанта на продуктовый код: условие #427, потеря
флага замены, схлопывание кодов ошибок.

Гейты: npm test 1806 tests, 1805 pass, 0 fail (было 1791);
mutation-gate --check зелёный. Смок локально не прогнать — Chromium в
песочнице нет, проверит CI.

Issue: #433
User-Visible: no
2026-09-03 13:21:08 +03:00
claude[bot] a6a9757b6d docs: review document for #432
Issue: #432
User-Visible: no
2026-09-03 10:09:05 +00:00
Sergey Matyunin d8e67f530c test(assets): isolate HA asset fixtures
Issue: #432
User-Visible: no
2026-09-03 12:51:20 +03:00
Sergey Matyunin f3c32fb203 fix(assets): bound resolve integrity work
Issue: #432
User-Visible: yes
2026-09-03 12:48:08 +03:00
claude[bot] 58df908db2 docs: review document for #432
Issue: #432
User-Visible: no
2026-09-03 09:38:37 +00:00
Sergey Matyunin 17a1c10bef docs(spec): define bounded asset resolution
Issue: #432
User-Visible: no
2026-09-03 12:32:33 +03:00
claude[bot] 431b462a16 docs: review document for #431
Issue: #431
User-Visible: no
2026-09-03 08:46:43 +00:00
Sergey Matyunin 6559679b63 build: synchronize issue 431 artifacts
Issue: #431
User-Visible: no
2026-09-03 11:33:40 +03:00
Sergey Matyunin 58efb6a1b2 fix(geometry): canonicalize decor image coordinates
Issue: #431
User-Visible: yes
2026-09-03 11:26:31 +03:00
claude[bot] d97bf63feb docs: review document for #431
Issue: #431
User-Visible: no
2026-09-03 08:21:10 +00:00
Sergey Matyunin 260af7bfd2 docs(spec): define image coordinate canonicalization
Issue: #431
User-Visible: no
2026-09-03 11:16:08 +03:00
claude[bot] 34853ba32a docs: review document for #428
Issue: #428
User-Visible: no
2026-09-03 08:09:36 +00:00
Sergey Matyuninandclaude[bot] 972b0e0c0a test(import): account for space id remapping
Issue: #428
User-Visible: no
2026-09-03 08:09:36 +00:00
Sergey Matyuninandclaude[bot] 2c4dd3f638 fix(import): preserve missing decor asset round-trips
Issue: #428
User-Visible: yes
2026-09-03 08:09:36 +00:00
claude[bot] 7cf0105d62 docs: review document for #428
Issue: #428
User-Visible: no
2026-09-03 08:09:36 +00:00
Sergey Matyuninandclaude[bot] 6f8ce48e5b docs(spec): define missing decor asset round-trip
Issue: #428
User-Visible: no
2026-09-03 08:09:36 +00:00
Claude 8b9548775f test(backend): HA-зависимый тест объявляет это сам, а не именем файла
`python3 -m pytest tests_backend/` без Home Assistant обрывался НА СБОРКЕ:
`test_coordinate_canonicalization.py` тянет HA через `store`, а
`collect_ignore_glob` в conftest отсекает только `test_ha_*.py`. Ни один
из трёх сотен чистых тестов при этом не выполнялся, хотя CLAUDE.md и
PROCESS.md §8 обещают ровно обратное. В CI дефект невидим: там HA есть и
список игнора пуст.

Признак «нужен ли файлу HA» был подменён признаком «как файл назван» —
та же конструкция, которая в #389 уронила 85 тестов с голым assert False.

Вариант владельца — третий: `pytest.importorskip("homeassistant")` в
самом файле, до импортов, которые тянут HA. Теперь это честный скип
(«1 skipped» вместо «Interrupted»), остальные файлы прогоняются, а в CI
не скипается ничего.

Обещанная проверка остальных файлов сделана пофайловым collect: из
двадцати HA требует ровно один непоименованный — этот. Чтобы второй не
появился молча, добавлен статический гейт: он читает импорты (не
исполняет), строит множество модулей интеграции, тянущих HA, — с
замыканием по относительным импортам, потому что `import_export`
зависит от HA только через `store`, — и требует у такого теста либо имя
`test_ha_*`, либо importorskip.

Свидетели, каждый проверен отрицательным прогоном:
- снять importorskip → красный «файлам нужен HA, но они этого не
  объявляют»;
- новый чистый файл с импортом store → тот же красный;
- убрать замыкание → красный синтетический тест сканера;
- перестать исключать TYPE_CHECKING → красный он же;
- считать импорты внутри функций → красный он же.
Плюс два свидетеля самого сканера в теле гейта: `store` обязан быть
найден, `coordinate_canonicalization` обязан остаться чистым — иначе
«ничего не нашёл» выглядело бы как «всё в порядке».

Мутант `pure-backend-test-pulls-home-assistant` в реестре: добавляет
импорт store в чистый test_projection.py, guard — этот гейт.

Гейты: npm test 1791 tests, 1790 pass, 0 fail; pytest без HA
312 passed, 3 skipped (было: Interrupted, 0 выполнено);
mutation-gate --check зелёный.

Issue: #436
User-Visible: no
2026-09-03 11:06:25 +03:00
Claude bdf53dc482 docs: §2.7 требует таблицу «чем краснеет» у защитных AC
§2.7 уже требовал нужного: ревьюер обязан убедиться, что тест умеет
падать. Требование было, артефакта у него не было — в документе ревью
ничто не заставляло назвать, чем именно проверка краснеет, и «убедился»
писалось по впечатлению от чтения теста.

Цена известна по факту. Аудит v1.71.0-beta.1 нашёл пять защитных
контрактов #51 и #423, где снятие защиты не покраснило ни один тест
(#430). Все пять прошли код-ревью как доказанные; два теста попали в
закрытие coverage-ratchet под именами, обещавшими то, чего они не
проверяли.

Теперь у защитного AC три столбца: чем доказан, и отдельно — чем
краснеет, с результатом прогона. Пустой третий столбец — Medium.

Считаются защитные AC без свидетеля, а не мутанты на подсистему: у #421
мутанты были, и дыра всё равно проехала. Число мутантов остаётся
признаком — у #423 их ноль, и именно там нашёлся тест, спрашивавший
регулярку, находит ли она подстроку, которую сам же и вырезал.

Границы записаны явно, чтобы правило не стало ритуалом: AC без заявки на
защиту его не касаются, а «проверено чтением» остаётся законным — только
это видно во втором столбце.

Гейты: npm test 1791 tests, 1790 pass, 0 fail.

Issue: #435
User-Visible: no
2026-09-03 10:58:12 +03:00
Claude 1a8a51f2d9 fix(gates): ключ смоков покрывает оснастку и пробы гарда
Прогон #2371 (ee678352) добавил в demo/guard/verify-guard.mjs пробу
гарда benchmark — и job «Смоки в браузере» была ПРОПУЩЕНА как
переиспользованная. Причина в HARNESS.smoke: ключ держал ровно
demo/smoke_*.mjs, а `demo/serve.mjs`, `demo/guard/**` и benchmark, который
проба запускает, в него не входили. Файл исполняется только в этой job,
его правка её ключ не меняла — проба уехала в dev, ни разу не
запустившись.

Пропущенная проверка выглядит точно как пройденная: это тот же дефект,
против которого заведён #430, только этажом выше — не тест не умеет
падать, а job не запускается. Заодно это значит, что правки #404 в
serve.mjs и любой из четырёх проб гарда всё это время могли быть
реюзнуты.

Критерий ключа теперь один и читаемый: всё, что job исполняет.
Benchmark'и берутся все, а не по имени, — любой может быть подключён к
пробам позже, и лишний прогон дешевле молчания. demo/fixtures/** не
добавлен: он в корпусе sourceFingerprint, который уже подмешан в ключ.

Тест проверяет обе половины: файлы попадают в оснастку И правка каждого
меняет ключ. На прежнем предикате он краснеет (проверено).

Гейты: npm test 1791 tests, 1790 pass, 0 fail. Этот коммит меняет ключ
смоков, поэтому job со смоками на нём прогонится по-настоящему — вместе
с той пробой, ради которой всё и затевалось.

Issue: #430
User-Visible: no
2026-09-03 10:38:47 +03:00
Claude ee678352c7 test(gates): свидетели пяти защитных контрактов #51 и #423
Аудит v1.71.0-beta.1 (§3.2 M1/M2/M3/M6/M9) прогнал по мутанту на каждый
контракт: пять снятий защиты не покраснили ни один тест. Восьмой подряд
случай проверок, не умеющих падать, и первый — в бете, закрывавшей #421,
задачу ровно об этом.

Каждый свидетель ниже проверен отрицательным прогоном: мутант краснит
ровно свой тест и не задевает остальные.

1. Полное декодирование растра. `test_supported_raster_headers_and_full_decode`
   спрашивал w/h/mime — их даёт header-парсер; обрезка `PNG_1X1[:33]`
   теряет IEND и отбивается там же. No-op блока Pillow оставлял 35 pass.
   Новый свидетель: PNG с верными сигнатурой, IHDR, IEND, длинами и CRC,
   но с текстом вместо zlib-потока в IDAT. Оба прежних теста
   переименованы — их имена обещали то, чего они не проверяли.
   Пропуск без Pillow остался (validate_asset глотает ImportError
   осознанно), но в окружении с Home Assistant отсутствие Pillow теперь
   красное само по себе — иначе свидетель молча скипался бы в каноне.

2. Канонизация SVG. `ValidatedAsset(canonical,…)` → `ValidatedAsset(data,…)`
   не краснило ничего: все тесты смотрели w/h/mime, ни один — байты.
   Свидетель сверяет байты целиком: пролог и комментарий не переживают
   канонизацию, пустой элемент сжимается.
   Вторая половина — `_check_size(canonical)`. Экранирование `>` в тексте
   раздувает документ вчетверо: загрузка 1.84 МиБ канонизуется в 7.35 МиБ
   и до сих пор проходила входной контроль. Свидетель — ровно такая.

3. Гард внешних URL. Все три «внешних» кейса корпуса ловились другими
   правилами (тег не из словаря, атрибут не из словаря, ветвь href), и
   `if False:` не краснило ничего. Свидетель: разрешённый тег, разрешённый
   атрибут, пять токенов — сработать может только сам гард, сообщение это
   подтверждает.

4. Проекция декора. Единственный кейс задавал flip_h и opacity: 2 → 1;
   ожидание неотличимо от «opacity игнорируется», а flip_v не проверялся
   вовсе. Свидетели: четыре комбинации отражений, opacity 0.4 и 0 против
   заглушки, форма asset_id с согласованным url (прежняя строка ловилась
   сравнением url, поэтому регулярку id можно было удалить незаметно).

5. Гард benchmark из #423. Доказательство было циклическим: тест вырезал
   из текста подстроку и спрашивал регулярку, находит ли она её, — а
   регулярка искала именно её. Динамический режим `--guard-probe`
   существовал с #423 и не вызывался ни одним прогоном. Теперь он в
   `demo/guard/verify-guard.mjs` (умеет аргументы и файл выше каталога),
   а тест из обнаружения требует у нового page-benchmark режим пробы и
   запись в верификаторе.

Мутантов в реестре стало восемь новых: четыре бэкендных, три юнитных,
один на пробах гарда. У #423 их было ноль — единственная задача с
циклическим тестом и она же единственная без мутантов.

Гейты: typecheck зелёный; npm test 1790 tests, 1789 pass, 0 fail;
pytest без HA 310 passed, 2 skipped; npm run build зелёный, dist не
изменился (продуктовый код не тронут); mutation-gate --check применяет
все восемь якорей. Полный прогон новых мутантов — следующим шагом.

Issue: #430
User-Visible: no
2026-09-03 10:28:00 +03:00
claude[bot] c3eb225c8f docs: review document for #427
Issue: #427
User-Visible: no
2026-09-03 07:20:27 +00:00
Sergey Matyunin f71de89975 docs: refresh screenshot source fingerprint
Issue: #427
User-Visible: no
2026-09-03 10:07:25 +03:00
Sergey Matyunin b87e99f9a1 fix: keep decor downscale action available
Issue: #427
User-Visible: yes
2026-09-03 10:04:45 +03:00
Claude 4cabcbe828 chore(gates): убрать числовой храповик бюджета из #423
Проверка `assertSupportBundleOwnership` бросала при
`initialViewGzipBytes >= 291046` — «граф не стал больше, чем на момент
закрытия #423». Запас до порога: пятнадцать байт на бете 1.71.0, сто
четыре сейчас. Пятнадцать байт gzip меньше одной строки локали, поэтому
первый же посторонний коммит получил бы красный CI с сообщением про
копирайт формы поддержки — к которому не имеет отношения.

«Граф не вырос» — критерий приёмки на момент задачи, а не свойство
продукта. Свойство продукта охраняют `INITIAL_VIEW_GZIP_BUDGET` (300000)
и предупреждение о низком запасе: они судят размер целиком и не
привязаны к чужому номеру issue.

Долговечная половина того же контракта — владение графом (маркеры формы
отсутствуют в initial View, присутствуют в ленивом редакторе) — осталась
и по-прежнему падает в обе стороны. Новый тест закрепляет, что размер в
эту функцию вернуться не может: 0, 291046, 10 МБ и `undefined` проходят
одинаково, пока владение соблюдено.

Гейты: typecheck зелёный, npm test 1787/1786 pass 0 fail, npm run build
зелёный (dist побайтово не изменился — продуктовый код не тронут),
node scripts/bundle-budget.mjs зелёный: initial View 290942 Б, бюджет
300000 Б, запас 9058 Б плюс честное предупреждение о запасе.

Issue: #429
User-Visible: no
2026-09-03 09:56:54 +03:00
claude[bot] cc09e8f827 docs: review document for #426
Проверка (CI) / Классификация изменённых файлов (push) Successful in 24s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Successful in 50s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 45s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 17s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 24s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 13m24s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 6m56s
Issue: #426
User-Visible: no
2026-09-03 06:38:29 +00:00
Sergey Matyunin a6eb3d6cfa test: accept room tooltip settings golden
Issue: #426
User-Visible: no
Release: v1.71.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33722471494
2026-09-03 09:25:14 +03:00
Sergey Matyunin 223951c042 chore: refresh config schema manifest
Issue: #426
User-Visible: no
2026-09-03 09:16:22 +03:00
Sergey Matyunin 9d1e5fdbba docs: refresh screenshot source fingerprint
Issue: #426
User-Visible: no
2026-09-03 09:12:50 +03:00
Sergey Matyunin 8013f7d052 feat: add room hover tooltip setting
Issue: #426
User-Visible: yes
2026-09-03 09:06:57 +03:00
claude[bot] ac99a2ff08 docs: review document for #426
Issue: #426
User-Visible: no
2026-09-03 05:52:52 +00:00
Sergey Matyunin 587c20699c docs: specify room hover tooltip toggle
Issue: #426
User-Visible: no
2026-09-03 08:47:07 +03:00
33 changed files with 362 additions and 765 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
ASSETS_DIR = "houseplan/assets"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.71.0-beta.2"
VERSION = "1.71.0-beta.1"
DECOR_ASSETS_API_VERSION = 1
# #43: the support transport is deliberately not configurable. A user supplied
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9",
"fingerprint": "aa228c5ecf6e913fe9a0058b57b44a53616b73c0bef1c20f84f88f7b56814274",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-D9kogu9G.js",
"houseplan-assets/houseplan-card-BF6i5sqv.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 291175,
"initialViewGzipBytes": 291067,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/de-B-zT3Bet.js",
"houseplan-assets/editor-DJZS-SMT.js",
"houseplan-assets/fr-DhlZhAWs.js",
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/de-RKtXul6Y.js",
"houseplan-assets/editor-D6wVlv9q.js",
"houseplan-assets/fr-Cp6oi90d.js",
"houseplan-assets/houseplan-editor-runtime-BENrZzdh.js",
"houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js"
],
"lazyGzipBytes": 215559,
"lazyGzipBytes": 215506,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/editor-DJZS-SMT.js",
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/editor-D6wVlv9q.js",
"houseplan-assets/houseplan-editor-runtime-BENrZzdh.js"
],
"lazyEditorGzipBytes": 159876,
"lazyEditorGzipBytes": 159815,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js"
],
"lazyOnboardingGzipBytes": 14133,
"lazyOnboardingGzipBytes": 14131,
"lazyLocaleFiles": [
"houseplan-assets/de-B-zT3Bet.js",
"houseplan-assets/fr-DhlZhAWs.js"
"houseplan-assets/de-RKtXul6Y.js",
"houseplan-assets/fr-Cp6oi90d.js"
],
"lazyLocaleGzipBytes": 48727,
"lazyLocaleGzipBytes": 48737,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BEQcDs80.js",
"sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805",
"path": "houseplan-assets/backdrop-pick-DeLnU0B5.js",
"sha256": "46e13a967a84ce24f3e31e01f3e51733efa61631b42e0ff3f94ed3daecac9ac2",
"rawBytes": 20996,
"gzipBytes": 7177,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-B-zT3Bet.js",
"sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1",
"path": "houseplan-assets/de-RKtXul6Y.js",
"sha256": "957316cc1ca9c874768c511bb282d4c603cea1eebb1af75170367d1288e1578d",
"rawBytes": 81632,
"gzipBytes": 24635,
"gzipBytes": 24640,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DJZS-SMT.js",
"sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4",
"path": "houseplan-assets/editor-D6wVlv9q.js",
"sha256": "304f38472f09805a10d812a4cd606b70d591bafae73885821c3b2c37a5cec49e",
"rawBytes": 3826,
"gzipBytes": 1580,
"gzipBytes": 1581,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DhlZhAWs.js",
"sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d",
"path": "houseplan-assets/fr-Cp6oi90d.js",
"sha256": "06efef049665b3f0caa94674f840f04768aca4bede2e9b715cb8cdd26cba028d",
"rawBytes": 83754,
"gzipBytes": 24092,
"gzipBytes": 24097,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-D9kogu9G.js",
"sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb",
"rawBytes": 1033646,
"gzipBytes": 290380,
"path": "houseplan-assets/houseplan-card-BF6i5sqv.js",
"sha256": "4a9deb476f6b6c2e0a074f42902603386cffde4205b3c734b989f439d8e3e74b",
"rawBytes": 1033268,
"gzipBytes": 290270,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-B-zT3Bet.js",
"houseplan-assets/editor-DJZS-SMT.js",
"houseplan-assets/fr-DhlZhAWs.js",
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
"houseplan-assets/de-RKtXul6Y.js",
"houseplan-assets/editor-D6wVlv9q.js",
"houseplan-assets/fr-Cp6oi90d.js",
"houseplan-assets/houseplan-editor-runtime-BENrZzdh.js",
"houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
"sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65",
"rawBytes": 576162,
"gzipBytes": 151119,
"path": "houseplan-assets/houseplan-editor-runtime-BENrZzdh.js",
"sha256": "a3a99a6b8c994d09499a8506c94a70e7fe9d6127d71c3edaa3240a830234b26c",
"rawBytes": 576032,
"gzipBytes": 151057,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js",
"sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2",
"path": "houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js",
"sha256": "006ccd4f8214f255f0009d5c4894f7bd6d07a6f6997a231515eb7ebb03977090",
"rawBytes": 28088,
"gzipBytes": 6956,
"gzipBytes": 6954,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d",
"sha256": "c9808b3cea6e687c2dd48e069809e35255f82db260183416256d1e6c70c97c11",
"rawBytes": 1183,
"gzipBytes": 795,
"gzipBytes": 797,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";import{b as e,l as o,t,di as s,A as a,dj as i,dk as l,dl as n,E as r,c}from"./houseplan-card-D9kogu9G.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa228c5ecf6e913fe9a0058b57b44a53616b73c0bef1c20f84f88f7b56814274";import{b as e,l as o,t,dg as s,A as a,dh as i,di as l,dj as n,E as r,c}from"./houseplan-card-BF6i5sqv.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(h.slice(0,3))}
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";try{await import("./houseplan-assets/houseplan-card-D9kogu9G.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa228c5ecf6e913fe9a0058b57b44a53616b73c0bef1c20f84f88f7b56814274";try{await import("./houseplan-assets/houseplan-card-BF6i5sqv.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.71.0-beta.2"
"version": "1.71.0-beta.1"
}
-69
View File
@@ -26,68 +26,6 @@ const result = await page.evaluate(async () => {
await card.updateComplete;
out.defaultRoomTip = card._tip?.room === true && !!root().querySelector('.tip');
const originalCallWS = card.hass.callWS;
const beforeRejectedSave = structuredClone(card._serverCfg);
const beforeRejectedFingerprint = card._cfgContentFingerprint;
card._openSettingsDialog();
card._settingsDialog = {
...card._settingsDialog,
colors: {
...card._settingsDialog.colors,
light_on: { c: '#123456', a: 0.4 },
},
glowRadius: 4,
bgColor: '#234567',
northDeg: 45,
bgMode: 'static',
sunRays: true,
showRoomTooltip: false,
};
card.hass = { ...card.hass, callWS: async (message) => {
if (message.type === 'houseplan/config/set') throw new Error('offline');
return originalCallWS(message);
} };
await card._saveSettingsDialog();
await card.updateComplete;
out.rejectedSettingsRolledBack = JSON.stringify(card._serverCfg)
=== JSON.stringify(beforeRejectedSave);
out.rejectedFingerprintRolledBack = card._cfgContentFingerprint
=== beforeRejectedFingerprint;
out.rejectedDraftKept = card._settingsDialog?.showRoomTooltip === false
&& card._settingsDialog?.bgColor === '#234567'
&& card._settingsDialog?.colors.light_on.c === '#123456';
out.rejectedBusyCleared = card._settingsDialog?.busy === false;
card._tip = null;
room().dispatchEvent(mouse('pointermove', 185, 185));
await card.updateComplete;
out.rejectedRuntimeUsesServerSettings = card._tip?.room === true;
const authoritative = structuredClone(beforeRejectedSave);
authoritative.settings = { ...authoritative.settings, bg_color: '#345678' };
const authoritativeRev = card._cfgRev + 5;
card._openSettingsDialog();
card._settingsDialog = {
...card._settingsDialog, bgColor: '#abcdef', showRoomTooltip: false,
};
card.hass = { ...card.hass, callWS: async (message) => {
if (message.type === 'houseplan/config/set') {
throw Object.assign(new Error('conflict'), { code: 'conflict' });
}
if (message.type === 'houseplan/config/get') {
return { config: structuredClone(authoritative), rev: authoritativeRev, can_write: true };
}
return originalCallWS(message);
} };
await card._saveSettingsDialog();
await card.updateComplete;
out.conflictKeepsAuthoritative = card._serverCfg.settings.bg_color === '#345678'
&& !Object.hasOwn(card._serverCfg.settings, 'show_room_tooltip')
&& card._cfgRev === authoritativeRev;
out.conflictDraftKept = card._settingsDialog?.bgColor === '#abcdef'
&& card._settingsDialog?.showRoomTooltip === false
&& card._settingsDialog?.busy === false;
card.hass = { ...card.hass, callWS: originalCallWS };
card._openSettingsDialog();
card._settingsDialog = { ...card._settingsDialog, showRoomTooltip: false };
await card._saveSettingsDialog();
@@ -132,13 +70,6 @@ checkAll(result, {
defaultOn: true,
cancelKeepsAbsentDefault: true,
defaultRoomTip: true,
rejectedSettingsRolledBack: true,
rejectedFingerprintRolledBack: true,
rejectedDraftKept: true,
rejectedBusyCleared: true,
rejectedRuntimeUsesServerSettings: true,
conflictKeepsAuthoritative: true,
conflictDraftKept: true,
falsePersists: true,
visibleRoomTipCleared: true,
disabledRoomTip: true,
+56 -56
View File
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9",
"fingerprint": "aa228c5ecf6e913fe9a0058b57b44a53616b73c0bef1c20f84f88f7b56814274",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-D9kogu9G.js",
"houseplan-assets/houseplan-card-BF6i5sqv.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 291175,
"initialViewGzipBytes": 291067,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/de-B-zT3Bet.js",
"houseplan-assets/editor-DJZS-SMT.js",
"houseplan-assets/fr-DhlZhAWs.js",
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/de-RKtXul6Y.js",
"houseplan-assets/editor-D6wVlv9q.js",
"houseplan-assets/fr-Cp6oi90d.js",
"houseplan-assets/houseplan-editor-runtime-BENrZzdh.js",
"houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js"
],
"lazyGzipBytes": 215559,
"lazyGzipBytes": 215506,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/editor-DJZS-SMT.js",
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/editor-D6wVlv9q.js",
"houseplan-assets/houseplan-editor-runtime-BENrZzdh.js"
],
"lazyEditorGzipBytes": 159876,
"lazyEditorGzipBytes": 159815,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js"
],
"lazyOnboardingGzipBytes": 14133,
"lazyOnboardingGzipBytes": 14131,
"lazyLocaleFiles": [
"houseplan-assets/de-B-zT3Bet.js",
"houseplan-assets/fr-DhlZhAWs.js"
"houseplan-assets/de-RKtXul6Y.js",
"houseplan-assets/fr-Cp6oi90d.js"
],
"lazyLocaleGzipBytes": 48727,
"lazyLocaleGzipBytes": 48737,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BEQcDs80.js",
"sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805",
"path": "houseplan-assets/backdrop-pick-DeLnU0B5.js",
"sha256": "46e13a967a84ce24f3e31e01f3e51733efa61631b42e0ff3f94ed3daecac9ac2",
"rawBytes": 20996,
"gzipBytes": 7177,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-B-zT3Bet.js",
"sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1",
"path": "houseplan-assets/de-RKtXul6Y.js",
"sha256": "957316cc1ca9c874768c511bb282d4c603cea1eebb1af75170367d1288e1578d",
"rawBytes": 81632,
"gzipBytes": 24635,
"gzipBytes": 24640,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DJZS-SMT.js",
"sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4",
"path": "houseplan-assets/editor-D6wVlv9q.js",
"sha256": "304f38472f09805a10d812a4cd606b70d591bafae73885821c3b2c37a5cec49e",
"rawBytes": 3826,
"gzipBytes": 1580,
"gzipBytes": 1581,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DhlZhAWs.js",
"sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d",
"path": "houseplan-assets/fr-Cp6oi90d.js",
"sha256": "06efef049665b3f0caa94674f840f04768aca4bede2e9b715cb8cdd26cba028d",
"rawBytes": 83754,
"gzipBytes": 24092,
"gzipBytes": 24097,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-D9kogu9G.js",
"sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb",
"rawBytes": 1033646,
"gzipBytes": 290380,
"path": "houseplan-assets/houseplan-card-BF6i5sqv.js",
"sha256": "4a9deb476f6b6c2e0a074f42902603386cffde4205b3c734b989f439d8e3e74b",
"rawBytes": 1033268,
"gzipBytes": 290270,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-B-zT3Bet.js",
"houseplan-assets/editor-DJZS-SMT.js",
"houseplan-assets/fr-DhlZhAWs.js",
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
"houseplan-assets/de-RKtXul6Y.js",
"houseplan-assets/editor-D6wVlv9q.js",
"houseplan-assets/fr-Cp6oi90d.js",
"houseplan-assets/houseplan-editor-runtime-BENrZzdh.js",
"houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
"sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65",
"rawBytes": 576162,
"gzipBytes": 151119,
"path": "houseplan-assets/houseplan-editor-runtime-BENrZzdh.js",
"sha256": "a3a99a6b8c994d09499a8506c94a70e7fe9d6127d71c3edaa3240a830234b26c",
"rawBytes": 576032,
"gzipBytes": 151057,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js",
"sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2",
"path": "houseplan-assets/houseplan-onboarding-runtime-B0I6_T_V.js",
"sha256": "006ccd4f8214f255f0009d5c4894f7bd6d07a6f6997a231515eb7ebb03977090",
"rawBytes": 28088,
"gzipBytes": 6956,
"gzipBytes": 6954,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BEQcDs80.js",
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/backdrop-pick-DeLnU0B5.js",
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d",
"sha256": "c9808b3cea6e687c2dd48e069809e35255f82db260183416256d1e6c70c97c11",
"rawBytes": 1183,
"gzipBytes": 795,
"gzipBytes": 797,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-D9kogu9G.js"
"houseplan-assets/houseplan-card-BF6i5sqv.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";import{b as e,l as o,t,di as s,A as a,dj as i,dk as l,dl as n,E as r,c}from"./houseplan-card-D9kogu9G.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa228c5ecf6e913fe9a0058b57b44a53616b73c0bef1c20f84f88f7b56814274";import{b as e,l as o,t,dg as s,A as a,dh as i,di as l,dj as n,E as r,c}from"./houseplan-card-BF6i5sqv.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(h.slice(0,3))}
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";try{await import("./houseplan-assets/houseplan-card-D9kogu9G.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa228c5ecf6e913fe9a0058b57b44a53616b73c0bef1c20f84f88f7b56814274";try{await import("./houseplan-assets/houseplan-card-BF6i5sqv.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
-7
View File
@@ -2,13 +2,6 @@
## Unreleased
- If Home Assistant rejects a General settings save, the plan now returns to
the last server-confirmed state instead of displaying an unsaved local draft;
the entered values remain in the open dialog for another attempt
([#439](https://github.com/Matysh/houseplan-card/issues/439)).
## v1.71.0-beta.2 — 2026-09-03
- Custom background images now recover cleanly after interrupted uploads,
consume quota consistently, retry after configuration changes and remain
compatible with older integrations; language switching and failed support
-7
View File
@@ -8,13 +8,6 @@
## Не выпущено
- Если Home Assistant отклоняет сохранение общих настроек, план снова показывает
последнее подтверждённое сервером состояние вместо несохранённого локального
черновика; введённые значения остаются в открытом диалоге для повторной
попытки ([#439](https://github.com/Matysh/houseplan-card/issues/439)).
## v1.71.0-beta.2 — 2026-09-03
- Пользовательские картинки подложки теперь корректно восстанавливаются после
прерванной загрузки, всегда учитываются в лимите, повторно проверяются после
изменения конфигурации и совместимы со старыми версиями интеграции; смена
+9 -9
View File
@@ -1,18 +1,18 @@
<!-- release: v1.71.0-beta.2 -->
<!-- release: v1.71.0-beta.1 -->
## Основное
- Пользовательские картинки подложки теперь безопасно уменьшаются, восстанавливаются после прерванной загрузки и отсутствующего файла, а их координаты остаются стабильными после сохранения и оптимизации плана ([#427](https://github.com/Matysh/houseplan-card/issues/427), [#428](https://github.com/Matysh/houseplan-card/issues/428), [#431](https://github.com/Matysh/houseplan-card/issues/431), [#434](https://github.com/Matysh/houseplan-card/issues/434)).
- Доступ к сохранённым картинкам подложки стал безопаснее и быстрее: произвольный поиск файлов закрыт, а повторные загрузки используют ограниченную кэшированную проверку целостности ([#432](https://github.com/Matysh/houseplan-card/issues/432), [#434](https://github.com/Matysh/houseplan-card/issues/434)).
- В общих настройках теперь можно отключить информационное окно комнаты при наведении, сохранив подсветку комнаты и подсказки устройств ([#426](https://github.com/Matysh/houseplan-card/issues/426)).
- В редакторе подложки теперь можно загружать и повторно использовать закрытые PNG, JPEG, WebP и безопасные SVG, а затем размещать и редактировать их как мебель ([#51](https://github.com/Matysh/houseplan-card/issues/51)).
- «Помощь и обратная связь» и диалоги опасных действий стали надёжнее при смене языка, пространства или версии, повторной отправке и некорректном конфиге ([#417](https://github.com/Matysh/houseplan-card/issues/417), [#418](https://github.com/Matysh/houseplan-card/issues/418), [#420](https://github.com/Matysh/houseplan-card/issues/420), [#423](https://github.com/Matysh/houseplan-card/issues/423)).
- Автоматическая привязка к зонам Home Assistant больше не забывает устройства из-за временно пустого или неполного реестра ([#419](https://github.com/Matysh/houseplan-card/issues/419)).
- Мелкие исправления и улучшения.
## Highlights
- Custom background images can now be safely reduced, recovered after interrupted uploads or missing files, and keep stable coordinates across saves and plan optimization ([#427](https://github.com/Matysh/houseplan-card/issues/427), [#428](https://github.com/Matysh/houseplan-card/issues/428), [#431](https://github.com/Matysh/houseplan-card/issues/431), [#434](https://github.com/Matysh/houseplan-card/issues/434)).
- Access to saved background images is safer and faster: arbitrary file lookup is blocked, while repeated loads reuse a bounded cached integrity check ([#432](https://github.com/Matysh/houseplan-card/issues/432), [#434](https://github.com/Matysh/houseplan-card/issues/434)).
- General settings can now hide the room information window on hover while keeping room highlighting and device tooltips ([#426](https://github.com/Matysh/houseplan-card/issues/426)).
- The Background editor can now upload and reuse private PNG, JPEG, WebP and safe SVG images, then place and edit them like furniture ([#51](https://github.com/Matysh/houseplan-card/issues/51)).
- Help & feedback and dangerous-action dialogs are now more reliable across language, space and version changes, edited retries, and malformed configuration ([#417](https://github.com/Matysh/houseplan-card/issues/417), [#418](https://github.com/Matysh/houseplan-card/issues/418), [#420](https://github.com/Matysh/houseplan-card/issues/420), [#423](https://github.com/Matysh/houseplan-card/issues/423)).
- Home Assistant Area relocation tracking no longer forgets devices because the registry is temporarily empty or incomplete ([#419](https://github.com/Matysh/houseplan-card/issues/419)).
- Small fixes and improvements.
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.71.0-beta.2/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.71.0-beta.2/docs/CHANGELOG.md)
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.71.0-beta.1/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.71.0-beta.1/docs/CHANGELOG.md)
+3 -3
View File
@@ -17,12 +17,12 @@ change must pass through a published beta/RC before stable. Stable release
commits are promotion-only (versions, generated bundles and release/changelog
metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
## Snapshot (2026-09-03)
## Snapshot (2026-09-02)
| Item | State |
|---|---|
| Version | **v1.71.0-beta.2** everywhere (manifest, const.py, package.json, CARD_VERSION) |
| Current local cycle | v1.71.0-beta.2 hardens custom Background images across oversized sources, interrupted uploads, missing-file export/import, coordinate canonicalization, bounded integrity checks and old-integration compatibility (#427, #428, #431, #432, #434). It also adds the option to hide the room information window without disabling room highlighting or device tooltips (#426). Publish only after exact-SHA Validate is green. |
| Version | **v1.71.0-beta.1** everywhere (manifest, const.py, package.json, CARD_VERSION) |
| Current local cycle | v1.71.0-beta.1 is the first prerelease candidate after stable v1.70.0. It adds reusable private raster/SVG images to the Background editor (#51), hardens confirmation and Help & feedback races/privacy/compatibility (#417, #418, #420, #423), and protects HA Area relocation provenance from incomplete registries (#419). Test and capture gates #421, #422, #424 and #425 ship in the same candidate. Publish only after exact-SHA Validate is green. |
| Hidden Labs Stage | #89 Stage 1 ships in v1.63.0-beta.1. #122 Stage 2 ships in v1.64.0 and evolves the same hidden, expiring `iso` experiment with matte walls, a low exterior floor edge, restrained shared shadows and live vertical door/window/gate panels. Flat remains default; editors and `houseplan-space-card` remain flat; live floor effects and HA actions remain unchanged. Public activation remains a separate task. |
| Workflow | Superseded 2026-08-12: the pre-1.62 rule of "local edits without tests or commits" is **dead** — since release 1.62 every product change follows `PROCESS.md` (issue in `S5-ready`+, branch `issue/<NN>-slug`, trailers on every commit, review pipeline; `AGENTS.md` is the summary). Release mechanics below remain current. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first); every bullet links its GitHub issue (#NN) so the #328 rules stay machine-checkable. A STABLE body aggregates the changelog since the PREVIOUS STABLE release (never since the last beta): features/fixes described across the line's beta changelogs must appear, while bugs that were introduced and fixed strictly inside the beta line (never shipped in any stable) are excluded — draft with `npm run release:notes -- <tag>`, curate by hand, then `npm run release:notes -- <tag> --verify` must pass. `Мелкие исправления и улучшения` / `Small fixes and improvements` is allowed only when the range really contains user-visible work not itemised in the body; a single-issue hotfix ships without it (the verifier enforces this). Every body ends with separate links to the Russian and English changelogs. Open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records; their labels carry priority and workflow status (`PROCESS.md` §9). GitHub Projects is no longer used. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
+11 -11
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceFingerprint": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"captureScriptSha256": "cadb8e1bcab9f1dcdd7d75b3b90ddcbaaeb2b8c2a098f575a21f39ff70f5c59c",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,7 +15,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "80a70361dc18dd0461568df332062e6482c633af5d280954f8b675701418a76d"
},
"view-touch": {
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "4106cc28847047505f46921ff95765d8abdf5b382d9d17d4c5e4ad129dd8f6be"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "617b51b3648498787b5039980c9f3eceb75ba56ed63a1a20e616bc05bc304362"
},
"room-contour-close": {
@@ -48,7 +48,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "1dab6cc3b9d1bf7d8c40f0e5137f8c688683c9b7eabc5167da99d41ecfdd5b79"
},
"plan-context-tray": {
@@ -59,7 +59,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "94ef50762753c0ac6ddc84d2521c4232a3f9ecd89843810d2df61c517592bed7"
},
"device-editor": {
@@ -70,7 +70,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "7a601769de38aa19c2e280f6ff4cf3695b854b1d1c6d1f8de4e6b5e8f7eb1559"
},
"device-display-preview": {
@@ -81,7 +81,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "0939875f8631694f4de0ef4fd01032010ede8c7d49fb7c0a857612d4b3afff93"
},
"background-editor": {
@@ -92,7 +92,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
},
"room-card": {
@@ -103,7 +103,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
},
"device-info": {
@@ -114,7 +114,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
"imageSha256": "cb37f7eefd936f98ef44969b21dfe8e40ee27d1f558abe8d53c0223f6488b8a6"
}
},
-221
View File
@@ -1,221 +0,0 @@
# CODE-REVIEW-439-r1
Issue: #439 — «Отказ сохранения настроек пространства оставляет применённым то,
чего на сервере нет»
Ветка: `issue/439-settings-save-rollback`
SHA на момент ревью: `3883352f` (родитель `f0574c1f`)
Трек: `trivial` (короткий, ТЗ в теле issue). Заход r1, блокирующих циклов
израсходовано 0 из 2.
## Скоуп
Диалог общих настроек пространства применял новый `settings` к
`_serverCfg` **оптимистично**, до подтверждения записи сервером
(`houseplan/config/set`). При обычном отказе записи (не `conflict`) откат не
происходил: карточка продолжала показывать непринятые сервером настройки, а
`show_room_tooltip` (и любая другая опция) вела себя так, будто запись
прошла — при том что ТЗ #426 объявляло этот риск снятым.
Три AC короткого трека:
- **AC1** — обычный отказ: `_serverCfg.settings` и его content fingerprint
откатываются к снимку до нажатия «Сохранить»; диалог остаётся открыт с
введённым draft, `busy=false`, тост ошибки.
- **AC2** — конфликт: авторитетный `config/get` после `conflict` не
перезаписывается локальным rollback-снимком.
- **AC3** — успех: не регрессирует.
Изменения: `src/serialized-write-queue.ts` (+49, новые
`optimisticAttempt`/`rollbackOptimistic`), `src/houseplan-editor-runtime.ts`
(точка вызова в `_saveSettingsDialog`, ~10 строк), новый браузерный smoke
`demo/smoke_room_tooltip_toggle.mjs` (+69), юнит-тесты на сам механизм,
оба changelog, собранный бандл (класс D), принятый fingerprint
документационных скриншотов.
## Как проверялось
### Чтение кода (доказательство по AC)
- **AC1.** `_saveSettingsDialog` (`src/houseplan-editor-runtime.ts:10228+`)
до оптимистичного присваивания сохраняет `attempt =
optimisticAttempt(cfg, nextConfig, _cfgContentFingerprint, _cfgRev,
contentFingerprint)` — глубокий клон предыдущего `_serverCfg` плюс его
fingerprint и текущую ревизию. В `catch` вызывается `rollbackOptimistic`,
которая переписывает `_serverCfg`/`_cfgContentFingerprint` обратно и
дёргает `requestUpdate()`, **только если** `host._cfgRev` не сдвинулась и
текущий `_serverCfg` либо тот же объект, либо содержательно совпадает с
неудавшейся попыткой. `_cfgRev` в `_sendConfigCandidate`
(`houseplan-card.ts:7677-7683`) обновляется **только после** успешного
`callWS` — на отказе строка `this._cfgRev = response?.rev …` не
выполняется, значит guard пропускает откат. Проверено чтением и
подтверждено smoke (ниже).
- **AC2.** Конфликтная ветка `_saveConfigNow`
(`houseplan-editor-runtime.ts:8930-8941`) уже до правки перечитывала
конфиг через `_reloadConfigOnly()` и **до** повторного throw. Ревизия из
ответа `config/get` монотонно больше ожидавшейся (иначе `conflict` не
возник бы), поэтому к моменту, когда внешний `catch` в
`_saveSettingsDialog` вызывает `rollbackOptimistic`, `host._cfgRev !==
attempt.revision` истинно и функция возвращает `false`, не трогая уже
авторитетный `_serverCfg`. Проверено чтением и smoke.
- **AC3.** Успешный путь `attempt` не читает вовсе — ветка `try` не
изменена в части закрытия диалога/применения настроек. Регрессия
подтверждена существующими и новыми smoke-проверками.
### Гейты, которые прогнал сам (на SHA `3883352f`, зелёного Validate на нём не найдено)
| Гейт | Команда | Результат |
|---|---|---|
| Тайпчек | `npx tsc --noEmit` | чисто, без вывода |
| Юнит-тесты | `npm test` | 1814 passed, 0 failed, 1 skipped — совпадает с тем, что заявил автор |
| Сборка + сверка 3 копий бандла | `npm run build`, затем `npm run bundle:sync` | сборка чистая; `git status --porcelain` после обеих команд пуст — `dist/`, `custom_components/.../frontend/`, `demo/srv/assets` уже в актуальном состоянии, commit не разошёлся с исходником |
| Документационный фингерпринт | `node scripts/check-docs.mjs` (diff трогает `src/**`) | «Documentation checks passed (7 files, 10 external links)» — подтверждает принятый в `3883352f` fingerprint корректен |
| Named smoke (AC) | `node demo/smoke_room_tooltip_toggle.mjs` | все 21 проверка `true`, `OK` |
| Дисциплина «тест умеет падать» | временно заменил `if (attempt) rollbackOptimistic(...)` на no-op, пересобрал бандл, перезапустил тот же smoke | покраснели ровно 3 проверки: `rejectedSettingsRolledBack`, `rejectedFingerprintRolledBack`, `rejectedRuntimeUsesServerSettings` — то есть именно AC1. Дерево восстановлено (`cp` бэкапа + `bundle:sync`), `git status --porcelain` снова пуст |
| Бюджет бандла (дёшево, для полноты) | `npm run bundle:budget` | initial View 291175 B gzip / 300000 бюджет, есть предупреждение о запасе <15000 Б (#367) — это существующее давление на бюджет всего проекта, не следствие 40 строк этого диффа; не блокирует |
### Выбор смоков по дельте
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`:
изменено файлов `src/**`: 2, символов на изменённых строках: 9, матрица 216
смоков (`ls demo/smoke_*.mjs | wc -l` = 216).
**Прямое совпадение (4)** — прогнал все:
- `demo/smoke_room_tooltip_toggle.mjs` — назван в AC, см. выше.
- `demo/smoke_area_relocation_safety.mjs` — зелёный, все 11 проверок `true`.
- `demo/smoke_v8_draft_write.mjs` — зелёный, все 16 проверок `true`.
- `demo/smoke_optimize_coordinate_canonicalization.mjs` — зелёный, все 17
проверок `true`.
**Слабая связь — одно распространённое имя (20, `_cfgRev`)** — не прогонял.
Решение: диф ограничен единственной точкой входа —
`catch`-блоком `_saveSettingsDialog`, который не переиспользуется другими
диалогами/потоками записи; `_cfgRev` в остальных 20 смоках фигурирует по
несвязанным путям (drag, layout, virtual light, discovery-фильтры и т.д.),
ни один из них не проходит через `_saveSettingsDialog`. Считаю прогон
избыточным для этой дельты.
Golden/perf/backend: не прогонял — diff не меняет геометрию, стили или слои
в успешном пути (единственное видимое отличие — поведение при уже
подтверждённом отказе записи, вне сценариев golden-капчура); backend
(`custom_components/**/*.py`) не тронут; perf не назван в AC. Инварианты
модели (`npm run invariants`) не прогонял — diff не трогает рёбра комнат,
записи толщины, `layout`, `marker.space`, `open_spans`.
## Находки
Ни одной блокирующей (High) или Medium-в-скоупе находки. Одна Low,
снимается с записью ниже — без отдельного issue (в скоуп не выходит).
**Low — обещанный в самом ТЗ второй сдвиг `_cfgEpoch` не реализован.**
Раздел «Реализация и границы» в теле issue: «Одновременно восстановить
`_cfgContentFingerprint`, сдвинуть cache epoch и запросить render.»
`rollbackOptimistic` (`src/serialized-write-queue.ts`) восстанавливает
`_serverCfg` и `_cfgContentFingerprint`, дёргает `requestUpdate()`, но
**не трогает `_cfgEpoch`** — тип `host` в её сигнатуре его даже не
перечисляет. `_cfgEpoch` бампается один раз в начале `_saveConfigNow`
(строка 8931) и второй раз после отката не сдвигается.
Разобрал, порождает ли это видимый дефект: `_cfgEpoch` участвует в ключах
кэшей геометрии/предпросмотра (контуры комнат, объединение стен, солнечные
лучи) и в редакторских кэшах декора — но геометрию/декор этот диалог не
меняет, а единственный settings-зависимый кэш, ключующийся по эпохе
(солнечные лучи, `houseplan-card.ts:10488`), включает `north`/значения
режима **прямо в строку ключа**, а не полагается на эпоху как на
единственный признак изменения. Поэтому даже кэш-промах после несостоявшегося
второго бампа не может отдать неверные лучи для другого `north` — ключ
и так другой; а для того же `north` результат совпадает по построению.
Цвета/glow/фон читаются из `_serverCfg.settings` в рендере без отдельного
эпоха-кэша. Видимого дефекта не нашёл ни рассуждением, ни smoke (который
как раз проверяет наблюдаемое поведение runtime после отката —
`rejectedRuntimeUsesServerSettings`).
Снимаю без правки: разрыв между текстом ТЗ и кодом реален, но не создаёт
наблюдаемого расхождения при нынешней архитектуре кэшей. Если в будущем
появится settings-зависимый эпоха-кэш без значения в ключе (например,
для `bg_color`/`glow_radius_cm`), тот же паттерн уже будет тихо ломаться —
стоит иметь в виду при следующей работе с `rollbackOptimistic`, но заводить
это отдельным issue сейчас избыточно: одно наблюдение без текущего
пользовательского эффекта.
## Что проверено и корректно
- Идентити-guard (`_cfgRev` + ссылочное/содержательное сравнение
`_serverCfg`) корректно различает «наш неудавшийся кандидат» и
«авторитетный конфликтный reload» / «более новую правку» — подтверждено
и чтением кода, и двумя юнит-тестами
(`test/serialized-write-queue.test.mjs`), и smoke AC2.
`_cfgRev` меняется исключительно после успешного ответа сервера
(`_sendConfigCandidate`), поэтому на обычном отказе guard всегда
пропускает откат — структурно, а не по счастливой случайности с таймингом.
- Глубокий клон снимка (`JSON.parse(JSON.stringify(previous))` в
`optimisticAttempt`) — восстановленный `_serverCfg` не делит ссылку с
последующими мутациями; проверено юнит-тестом
(`assert.notEqual(host._serverCfg, previous, ...)`).
- draft в диалоге не трогается веткой rollback — `_settingsDialog` в
`catch` только теряет `busy`; введённые значения остаются, что и требует
AC1/AC2 (smoke `rejectedDraftKept`, `conflictDraftKept`).
- `_saveConfigNow`'s conflict-ветка (`await
this.host._reloadConfigOnly()` до повторного throw) не переписана этим
диффом — новый код добавлен строго в внешнем `catch`, не пересекается с
существующей веткой rollback для физической геометрии
(`physicalGeometryRolledBack`, `_reloadRejectedPhysicalWrite`), которая
этот диф тоже не трогает.
- Оба changelog в том же коммите (`f0574c1f`), формулировка соответствует
фактическому поведению (AC1), не переобещает про AC2 (внутренняя защита,
не новое видимое поведение) — текст `docs/CHANGELOG.md` /
`docs/CHANGELOG.ru.md` сверен построчно.
- Трейлеры `Issue: #439` и `User-Visible: yes|no` на обоих коммитах на
месте; `User-Visible: yes` в `f0574c1f` сопровождается правкой обоих
changelog в этом же коммите.
- Классификация `trivial`: один диалог/модуль, 3 AC, ожидаемое поведение
зафиксировано контрактом #426 — критерии §5.1 выполнены, спор не вижу.
- Единственный источник числа/значения: диф не вводит новое отображаемое
значение — только восстанавливает существующий источник (`_serverCfg`)
к его прежнему состоянию; `test/single-source-numbers.test.mjs` в
прогоне `npm test` зелёный без изменений в его области.
## Чего не проверял и почему
- Полный набор из 216 смоков и `golden:verify`/`performance_smoke` — diff
не расширяется за пределы одного диалога, это предрелизная обязанность
(§8), не гейт этого ревью.
- 20 смоков со слабой связью по `_cfgRev` — см. обоснование выше.
- `npm run invariants` — diff не касается геометрии/толщины/`layout`.
- `python -m pytest tests_backend` — `custom_components/**/*.py` не
тронут.
- Ручное тестирование в браузере (вне smoke-харнесса) — не запускал,
цикл ревью не предполагает ручной прогон; smoke дублирует ровно тот же
DOM/hass-стенд.
- Кросс-языковая идентичность канонизации координат (JS↔Python,
`LATTICE_GRID_N=240`, `LATTICE_NOISE_STEPS=1e-4`) как потенциальный
источник unstable fingerprint после `canonicalizeConfigGeometry` внутри
`_writeConfig` — рассмотрел теоретически (если бы бэкенд не гарантировал
фиксированную точку, guard мог бы пропустить откат для конфигов с
«грязной» геометрией). Не нашёл сценария: backend
(`custom_components/houseplan/websocket_api.py`) канонизирует конфиг на
каждом `config/get`/`config/set` тем же алгоритмом, поэтому входящий
`_serverCfg` уже канонический и повторная канонизация — no-op по
построению. Не свежий риск этого диффа (канонизация всего конфига при
любой записи settings существовала и до #439); не заводил как находку.
## Вердикт
Все три AC доказаны: AC1/AC3 существующим+новым smoke и юнит-тестами,
AC2 — smoke плюс структурным чтением guard'а (ревизия монотонна только
вверх и обновляется исключительно на успехе). Единственная находка — Low,
снята с запиской, без наблюдаемого дефекта. Гейты, которые обязаны быть
зелёными на этом заходе, зелёные (пересчитано мной, не только заявлено
автором). Trivial-классификация обоснована.
**Вердикт: зелёный.**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/439-settings-save-rollback`, коммит `3883352f43e7` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `39c0bc135ef59643339eb2d2fbee5a52e4ad6b3f`
```
git log --all --format='%H %T' | grep 39c0bc135ef5
```
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
"version": "1.71.0-beta.2",
"version": "1.71.0-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
"version": "1.71.0-beta.2",
"version": "1.71.0-beta.1",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.71.0-beta.2",
"version": "1.71.0-beta.1",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+1 -1
View File
@@ -378,7 +378,7 @@ import {
} from './space-order';
import { applyOpeningMoves, mergeCollinearPartitions, spaceMergeGeometry } from './wall-merge';
const CARD_VERSION = '1.71.0-beta.2';
const CARD_VERSION = '1.71.0-beta.1';
const ENTRY_BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
const EDITOR_RETRY_ASSET = '__HOUSEPLAN_EDITOR_RETRY_ASSET__';
+12 -13
View File
@@ -242,7 +242,7 @@ import {
canonicalizePosition,
formatLatticeShiftCm,
} from './coordinate-canonicalization';
import { enqueueSerializedWrite, optimisticAttempt, rollbackOptimistic, type OptimisticAttempt } from './serialized-write-queue';
import { enqueueSerializedWrite } from './serialized-write-queue';
import { hasTranslation, langOf, t, type I18nKey } from './i18n';
import { supportT, type SupportI18nKey } from './i18n/support';
import {
@@ -358,7 +358,7 @@ import {
} from './space-order';
import { applyOpeningMoves, mergeCollinearPartitions, spaceMergeGeometry } from './wall-merge';
const CARD_VERSION = '1.71.0-beta.2';
const CARD_VERSION = '1.71.0-beta.1';
type ResizeLiveLabel = {
kind: 'length';
@@ -10228,7 +10228,6 @@ public _updateDecorStyle(next: DecorStyle): void {
const d = this.host._settingsDialog;
if (!d || d.busy) return;
this.host._settingsDialog = { ...d, busy: true };
let attempt: OptimisticAttempt<ServerConfig> | null = null;
try {
const cfg = this.host._serverCfg!;
const isDefault = JSON.stringify(d.colors) === JSON.stringify(DEFAULT_FILL_COLORS);
@@ -10240,7 +10239,8 @@ public _updateDecorStyle(next: DecorStyle): void {
else delete settings.glow_radius_cm;
if (d.bgColor) settings.bg_color = d.bgColor;
else delete settings.bg_color;
// Legacy absence means static; new installations/spaces materialise daynight.
// Sun background compatibility is explicit: legacy absence means static,
// while new installations and spaces materialise daynight.
if (d.northDeg !== null && Number.isInteger(d.northDeg) && d.northDeg >= 0 && d.northDeg <= 359)
settings.north_deg = d.northDeg;
else delete settings.north_deg;
@@ -10249,22 +10249,21 @@ public _updateDecorStyle(next: DecorStyle): void {
else delete settings.sun_rays;
if (d.showRoomTooltip) delete settings.show_room_tooltip;
else settings.show_room_tooltip = false;
// Old configs may still contain this accepted field, but weather no
// longer affects sunlight; saving general settings cleans it up.
// Legacy compatibility: old configs may still contain this accepted
// field, but weather no longer affects sunlight and the UI no longer
// exposes it. Saving general settings cleans the obsolete value up.
delete settings.weather_entity;
const nextConfig = { ...cfg, settings };
attempt = optimisticAttempt(cfg, nextConfig, this.host._cfgContentFingerprint,
this.host._cfgRev, contentFingerprint);
this.host._serverCfg = nextConfig;
this.host._serverCfg = { ...cfg, settings };
await this._saveConfigNow();
if (!d.showRoomTooltip && this.host._tip?.room) this.host._tip = null;
this.host._settingsDialog = null;
this.host.requestUpdate();
this.host._showToast(this.host._t('gs.saved'));
} catch (e: any) {
// Esc may close the dialog in flight; the toast must still fire (audit L3).
// Never overwrite an authoritative conflict reload or newer edit (#439).
if (attempt) rollbackOptimistic(this.host, attempt, contentFingerprint);
// audit L3: the dialog may have been closed (Esc) while the save was
// in flight — spreading null yields a truthy husk and the renderer
// then crashes, blanking the whole card. The toast below is the
// only remaining signal, so it must still fire.
if (this.host._settingsDialog) this.host._settingsDialog = { ...this.host._settingsDialog, busy: false };
this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) }));
}
-49
View File
@@ -9,52 +9,3 @@ export function enqueueSerializedWrite(
): Promise<void> {
return previous.catch(() => undefined).then(write);
}
export interface OptimisticAttempt<T> {
previous: T;
previousFingerprint: string;
revision: number;
attempted: T;
attemptedFingerprint: string;
}
/** Capture the server-backed value before exposing a write candidate locally. */
export function optimisticAttempt<T>(
previous: T,
attempted: T,
previousFingerprint: string,
revision: number,
fingerprint: (value: T) => string,
): OptimisticAttempt<T> {
return {
previous: JSON.parse(JSON.stringify(previous)) as T,
previousFingerprint,
revision,
attempted,
attemptedFingerprint: fingerprint(attempted),
};
}
/**
* Roll back only the failed candidate represented by this attempt. A conflict
* reload or a newer edit owns a different revision/content and must win.
*/
export function rollbackOptimistic<T>(
host: {
_serverCfg: T | null;
_cfgRev: number;
_cfgContentFingerprint: string;
requestUpdate: () => unknown;
},
attempt: OptimisticAttempt<T>,
fingerprint: (value: T) => string,
): boolean {
const current = host._serverCfg;
if (!current || host._cfgRev !== attempt.revision
|| (current !== attempt.attempted
&& fingerprint(current) !== attempt.attemptedFingerprint)) return false;
host._serverCfg = attempt.previous;
host._cfgContentFingerprint = attempt.previousFingerprint;
host.requestUpdate();
return true;
}
+1 -50
View File
@@ -1,11 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
enqueueSerializedWrite,
optimisticAttempt,
rollbackOptimistic,
} from '../test-build/serialized-write-queue.js';
import { enqueueSerializedWrite } from '../test-build/serialized-write-queue.js';
function deferred() {
let resolve;
@@ -55,48 +51,3 @@ test('a failed write does not poison the next queued edit (#224)', async () => {
await recovered;
assert.deepEqual(seen, ['failed', 'recovered']);
});
test('a rejected optimistic write restores only its own candidate (#439)', () => {
const fingerprint = (value) => JSON.stringify(value);
const previous = { settings: { enabled: true } };
const attempted = { settings: { enabled: false } };
const attempt = optimisticAttempt(previous, attempted, 'server-fingerprint', 7, fingerprint);
let updates = 0;
const host = {
_serverCfg: attempted,
_cfgRev: 7,
_cfgContentFingerprint: fingerprint(attempted),
requestUpdate: () => { updates += 1; },
};
assert.equal(rollbackOptimistic(host, attempt, fingerprint), true);
assert.deepEqual(host._serverCfg, previous);
assert.notEqual(host._serverCfg, previous, 'the rollback snapshot is isolated from later mutations');
assert.equal(host._cfgContentFingerprint, 'server-fingerprint');
assert.equal(updates, 1);
});
test('a conflict reload or newer mutation wins over a rejected candidate (#439)', () => {
const fingerprint = (value) => JSON.stringify(value);
const previous = { settings: { value: 'server-before' } };
const attempted = { settings: { value: 'draft' } };
const attempt = optimisticAttempt(previous, attempted, 'before', 3, fingerprint);
const authoritative = { settings: { value: 'server-after' } };
let updates = 0;
const host = {
_serverCfg: authoritative,
_cfgRev: 4,
_cfgContentFingerprint: fingerprint(authoritative),
requestUpdate: () => { updates += 1; },
};
assert.equal(rollbackOptimistic(host, attempt, fingerprint), false);
assert.equal(host._serverCfg, authoritative);
assert.equal(updates, 0);
host._cfgRev = 3;
host._serverCfg = { settings: { value: 'newer-local-edit' } };
assert.equal(rollbackOptimistic(host, attempt, fingerprint), false);
assert.equal(host._serverCfg.settings.value, 'newer-local-edit');
assert.equal(updates, 0);
});