mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
v1.44.8: an uploaded plan never reached the config
Found on the owner's install: the image lands in /config/houseplan/plans, the space keeps plan_url=null, the plan never shows and re-saving does not help. _saveSpaceDialog held a reference to the space object across the await that uploads the file. _reloadConfigOnly() — which runs on every houseplan_config_updated event — REPLACES _serverCfg, so that reference became an orphan: plan_url, aspect, title and every display setting were written into a detached object while the save shipped the untouched config. In 'create' mode the whole new space was lost the same way. - upload first, then touch the config; no reference is held across an await. - _saveConfigNow() sets _cfgWriting like the debounced writer, so a revision arriving mid-save defers its reload instead of replacing the config (audit L2 extended to this path). - demo/smoke_plan_upload_race.mjs: on v1.44.7 the sent config still carries the OLD plan_url and the created space is missing; passes here. The demo's config/get now returns a fresh object, as a real server does — returning the same reference is what hid this class of bug from the smoke layer. - DEVELOPMENT.md: the deploy target is custom_components/houseplan/frontend/, and deploy verification must go over HTTP. A copy placed next to __init__.py is served by nobody — that cost two deployments today. - docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
This commit is contained in:
@@ -13,7 +13,7 @@ FILES_URL = "/houseplan_files/files"
|
|||||||
CONTENT_URL = "/api/houseplan/content"
|
CONTENT_URL = "/api/houseplan/content"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.44.7"
|
VERSION = "1.44.8"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.44.7"
|
"version": "1.44.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Загрузка подложки: ссылка обязана долететь до конфига.
|
||||||
|
// Баг 2026-07-27 (найден на боевой установке): _saveSpaceDialog держал ссылку
|
||||||
|
// на объект пространства через await загрузки файла. Любое событие
|
||||||
|
// houseplan_config_updated в этот момент вызывает _reloadConfigOnly(), которое
|
||||||
|
// ЗАМЕНЯЕТ _serverCfg — и plan_url/aspect/settings уезжали в осиротевший
|
||||||
|
// объект, а на сервер уходил нетронутый конфиг. Симптом: файл на диске есть,
|
||||||
|
// подложки нет, пересохранение не помогает.
|
||||||
|
import { launch, checkAll, finish } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const base = c.hass.callWS;
|
||||||
|
let reloadDuringUpload = 0;
|
||||||
|
|
||||||
|
c.hass = { ...c.hass, callWS: async (m) => {
|
||||||
|
if (m.type === 'houseplan/plan/set') {
|
||||||
|
// пока файл «загружается», прилетает чужая ревизия конфига
|
||||||
|
reloadDuringUpload++;
|
||||||
|
await c._reloadConfigOnly(true);
|
||||||
|
return { ok: true, url: '/api/houseplan/content/plans/_/' + m.space_id + '.png?v=42' };
|
||||||
|
}
|
||||||
|
if (m.type === 'houseplan/config/set') { c.__sent = m.config; return { ok: true, rev: 99 }; }
|
||||||
|
if (m.type === 'houseplan/config/get') {
|
||||||
|
// сервер отдаёт СВЕЖИЙ объект, а не тот же самый — как в реальном HA
|
||||||
|
const r = await base(m);
|
||||||
|
return { ...r, config: JSON.parse(JSON.stringify(r.config)) };
|
||||||
|
}
|
||||||
|
return base(m);
|
||||||
|
} };
|
||||||
|
|
||||||
|
// редактирование существующего пространства: подложка + новый заголовок
|
||||||
|
c._openSpaceDialog('edit', 'f1'); await c.updateComplete;
|
||||||
|
c._spaceDialog = { ...c._spaceDialog, title: 'Ground', source: 'file',
|
||||||
|
planFile: { ext: 'png', b64: 'AAAA', aspect: 1.6 } };
|
||||||
|
await c._saveSpaceDialog(); await c.updateComplete;
|
||||||
|
|
||||||
|
out.reloadHappened = reloadDuringUpload === 1;
|
||||||
|
const sentF1 = (c.__sent?.spaces || []).find((s) => s.id === 'f1');
|
||||||
|
const liveF1 = (c._serverCfg?.spaces || []).find((s) => s.id === 'f1');
|
||||||
|
out.sentPlanUrl = sentF1?.plan_url;
|
||||||
|
out.sentAspect = sentF1?.aspect;
|
||||||
|
out.sentTitle = sentF1?.title;
|
||||||
|
out.livePlanUrl = liveF1?.plan_url;
|
||||||
|
out.dialogClosed = c._spaceDialog === null;
|
||||||
|
|
||||||
|
// создание пространства при том же сбое: оно должно доехать целиком
|
||||||
|
c._openSpaceDialog('create'); await c.updateComplete;
|
||||||
|
c._spaceDialog = { ...c._spaceDialog, title: 'Attic', source: 'file',
|
||||||
|
planFile: { ext: 'png', b64: 'BBBB', aspect: 0.8 } };
|
||||||
|
await c._saveSpaceDialog(); await c.updateComplete;
|
||||||
|
const attic = (c.__sent?.spaces || []).find((s) => s.title === 'Attic');
|
||||||
|
out.atticSaved = !!attic;
|
||||||
|
out.atticHasPlan = !!attic && typeof attic.plan_url === 'string' && attic.plan_url.includes('/content/plans/');
|
||||||
|
out.atticAspect = attic?.aspect;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
// зафиксировано прогоном на v1.44.8 и сверено с кодом
|
||||||
|
checkAll(res, {
|
||||||
|
reloadHappened: true,
|
||||||
|
sentPlanUrl: '/api/houseplan/content/plans/_/f1.png?v=42',
|
||||||
|
sentAspect: 1.6,
|
||||||
|
sentTitle: 'Ground',
|
||||||
|
livePlanUrl: '/api/houseplan/content/plans/_/f1.png?v=42',
|
||||||
|
dialogClosed: true,
|
||||||
|
atticSaved: true,
|
||||||
|
atticHasPlan: true,
|
||||||
|
atticAspect: 0.8,
|
||||||
|
});
|
||||||
|
await finish(browser);
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -1,5 +1,23 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.44.8 — 2026-07-27
|
||||||
|
- **An uploaded plan is actually attached to the space.** `_saveSpaceDialog`
|
||||||
|
held a reference to the space object across the `await` that uploads the
|
||||||
|
image. Every `houseplan_config_updated` event runs `_reloadConfigOnly()`,
|
||||||
|
which *replaces* `_serverCfg` — so the reference became an orphan and
|
||||||
|
`plan_url`, `aspect`, the title and all display settings were written into a
|
||||||
|
detached object while the save shipped the untouched config. The file landed
|
||||||
|
on disk, the plan never appeared, and re-saving could not help. Creating a
|
||||||
|
space in that window lost the space entirely.
|
||||||
|
The upload now happens *before* the config is touched, and nothing is held
|
||||||
|
across an await.
|
||||||
|
- **`_saveConfigNow` marks the write in flight** (`_cfgWriting`), like the
|
||||||
|
debounced writer already did, so a remote revision arriving mid-save defers
|
||||||
|
its reload instead of replacing the config underneath it (audit L2 extended
|
||||||
|
to this path).
|
||||||
|
- Regression test `demo/smoke_plan_upload_race.mjs` fails on v1.44.7 and passes
|
||||||
|
here.
|
||||||
|
|
||||||
## v1.44.7 — 2026-07-27
|
## v1.44.7 — 2026-07-27
|
||||||
- **Plan backgrounds are visible again (regression from v1.44.5).** Since the
|
- **Plan backgrounds are visible again (regression from v1.44.5).** Since the
|
||||||
content endpoint requires authentication, the card asks the backend to sign
|
content endpoint requires authentication, the card asks the backend to sign
|
||||||
|
|||||||
@@ -6,6 +6,24 @@
|
|||||||
> **Правило проекта:** оба файла пополняются в одном коммите с самим
|
> **Правило проекта:** оба файла пополняются в одном коммите с самим
|
||||||
> изменением — как и остальная документация (см. docs/STATUS.md).
|
> изменением — как и остальная документация (см. docs/STATUS.md).
|
||||||
|
|
||||||
|
## 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.7 — 2026-07-27
|
||||||
- **Подложки снова отображаются (регрессия с v1.44.5).** Эндпоинт с файлами
|
- **Подложки снова отображаются (регрессия с v1.44.5).** Эндпоинт с файлами
|
||||||
требует авторизации, поэтому карточка просит бэкенд подписать ссылку на план —
|
требует авторизации, поэтому карточка просит бэкенд подписать ссылку на план —
|
||||||
|
|||||||
+12
-2
@@ -51,9 +51,19 @@ cp dist/houseplan-card.js custom_components/houseplan/frontend/
|
|||||||
|
|
||||||
- SSH: port **323**, root, key `ha_jb` (the user uploads it to the chat; in the sandbox /tmp/ha_jb, chmod 600).
|
- SSH: port **323**, root, key `ha_jb` (the user uploads it to the chat; in the sandbox /tmp/ha_jb, chmod 600).
|
||||||
- JS: `scp -P 323 -i /tmp/ha_jb dist/houseplan-card.js root@ha.jbstudio.pro:/config/custom_components/houseplan/frontend/`
|
- JS: `scp -P 323 -i /tmp/ha_jb dist/houseplan-card.js root@ha.jbstudio.pro:/config/custom_components/houseplan/frontend/`
|
||||||
|
- **The `frontend/` subfolder is not optional.** `__init__.py` registers
|
||||||
|
`Path(__file__).parent / "frontend" / "houseplan-card.js"` as the static path.
|
||||||
|
A copy dropped next to `__init__.py` (…/houseplan/houseplan-card.js) is served
|
||||||
|
by nobody: md5 on the server matches, the browser still gets the old bundle,
|
||||||
|
and hours go into debugging a bug that was already fixed. Cost this mistake
|
||||||
|
once: 2026-07-27, two releases deployed into the void.
|
||||||
- The whole integration: tar c custom_components/houseplan (--exclude __pycache__) → tar x on the server.
|
- The whole integration: tar c custom_components/houseplan (--exclude __pycache__) → tar x on the server.
|
||||||
- **Verification is mandatory**: `md5sum` locally == on the server == `curl http://homeassistant:8123/houseplan_files/houseplan-card.js | md5sum`
|
- **Verification is mandatory, and it must go over HTTP** — comparing md5 against
|
||||||
(inside the SSH add-on `localhost` is NOT HA, use the host `homeassistant`).
|
the file you just copied proves nothing about what the browser receives. The
|
||||||
|
one check that counts:
|
||||||
|
`curl -s https://ha.jbstudio.pro/houseplan_files/houseplan-card.js | grep -o '1\.[0-9]*\.[0-9]*' | sort -u`
|
||||||
|
must print the version just built. (Inside the SSH add-on `localhost` is NOT
|
||||||
|
HA — use the host `homeassistant`.)
|
||||||
- Python changes require an HA restart (`ha core restart`, holds the connection until it finishes, HTTP
|
- Python changes require an HA restart (`ha core restart`, holds the connection until it finishes, HTTP
|
||||||
comes back up in 1–3 min). JS changes — just a page refresh (the static path is served
|
comes back up in 1–3 min). JS changes — just a page refresh (the static path is served
|
||||||
with no-cache).
|
with no-cache).
|
||||||
|
|||||||
+2
-2
@@ -15,12 +15,12 @@
|
|||||||
|
|
||||||
| Item | State |
|
| Item | State |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Version | **v1.44.7** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
|
| Version | **v1.44.8** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
|
||||||
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) |
|
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) |
|
||||||
| GitHub | https://github.com/Matysh/houseplan-card — `main` = releases up to **v1.40.1**; `dev` ahead with v1.40.2+ (speaker icons, kiosk). 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) |
|
| GitHub | https://github.com/Matysh/houseplan-card — `main` = releases up to **v1.40.1**; `dev` ahead with v1.40.2+ (speaker icons, kiosk). 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) |
|
||||||
| CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish |
|
| CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish |
|
||||||
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled; ~864 older open PRs but merge rate ≈180/mo; realistic ETA 1–3 months (checked 2026-07-24) |
|
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled; ~864 older open PRs but merge rate ≈180/mo; realistic ETA 1–3 months (checked 2026-07-24) |
|
||||||
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed **v1.44.7** via direct copy (HACS custom repo also installed) |
|
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed **v1.44.8** via direct copy (HACS custom repo also installed) |
|
||||||
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
|
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
|
||||||
| Tests | 121 frontend (node:test) + 12 pure backend + 12 HA-harness (CI, py3.13); ~30 demo smoke suites (headless chromium) |
|
| Tests | 121 frontend (node:test) + 12 pure backend + 12 HA-harness (CI, py3.13); ~30 demo smoke suites (headless chromium) |
|
||||||
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
|
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
|
||||||
|
|||||||
@@ -234,6 +234,11 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
are only reachable through /api/houseplan/content/… with a session; the
|
are only reachable through /api/houseplan/content/… with a session; the
|
||||||
old /houseplan_files/plans|files paths return 404 after a restart; old
|
old /houseplan_files/plans|files paths return 404 after a restart; old
|
||||||
stored URLs keep working (rewritten on read) [auto+manual]
|
stored URLs keep working (rewritten on read) [auto+manual]
|
||||||
|
- [ ] Plan upload survives a concurrent config revision (v1.44.8): with a second
|
||||||
|
tab open on the same plan, attach a background image in space settings —
|
||||||
|
the plan shows immediately, `plan_url` is in `.storage/houseplan.config`,
|
||||||
|
and the same holds when the space is being CREATED, not edited
|
||||||
|
[auto: smoke_plan_upload_race]
|
||||||
- [ ] Signed plan background (v1.44.7): a space whose plan lives on the content
|
- [ ] Signed plan background (v1.44.7): a space whose plan lives on the content
|
||||||
endpoint renders its background image with an `authSig` query — the plan is
|
endpoint renders its background image with an `authSig` query — the plan is
|
||||||
visible after a plain page load, and Home Assistant logs NO failed-login
|
visible after a plain page load, and Home Assistant logs NO failed-login
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.44.7",
|
"version": "1.44.8",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+36
-11
@@ -32,7 +32,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
import { langOf, t, type I18nKey } from './i18n';
|
||||||
|
|
||||||
const CARD_VERSION = '1.44.7';
|
const CARD_VERSION = '1.44.8';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
const LS_KEY = 'houseplan_card_layout_v1';
|
||||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
@@ -2969,12 +2969,30 @@ class HouseplanCard extends LitElement {
|
|||||||
const wasFirst = d.mode === 'create' && (this._serverCfg?.spaces.length || 0) === 0;
|
const wasFirst = d.mode === 'create' && (this._serverCfg?.spaces.length || 0) === 0;
|
||||||
this._spaceDialog = { ...d, busy: true };
|
this._spaceDialog = { ...d, busy: true };
|
||||||
try {
|
try {
|
||||||
|
const drawAspect = d.orientation === 'portrait' ? 0.707 : d.orientation === 'square' ? 1 : 1.414;
|
||||||
|
const spaceId = d.mode === 'create' ? 's' + Date.now().toString(36) : d.spaceId!;
|
||||||
|
|
||||||
|
/* Upload BEFORE touching the config, and never hold a reference to a
|
||||||
|
config object across an await. `houseplan/config/get` runs on every
|
||||||
|
`houseplan_config_updated` event and REPLACES `_serverCfg`; a space
|
||||||
|
object captured before the upload is then detached, so plan_url,
|
||||||
|
aspect and settings were written into an orphan and the save shipped
|
||||||
|
the untouched config. Symptom: the file lands on disk, the plan never
|
||||||
|
appears, and re-saving does not help (owner's install, 2026-07-27). */
|
||||||
|
let uploaded: { url: string; aspect: number } | null = null;
|
||||||
|
if (d.source === 'file' && d.planFile) {
|
||||||
|
const resp = await this.hass.callWS({
|
||||||
|
type: 'houseplan/plan/set', space_id: spaceId, ext: d.planFile.ext, data: d.planFile.b64,
|
||||||
|
});
|
||||||
|
uploaded = { url: resp.url, aspect: d.planFile.aspect };
|
||||||
|
}
|
||||||
|
|
||||||
|
// from here on: no awaits until the save, so `sp` cannot be orphaned
|
||||||
const cfg = this._serverCfg!;
|
const cfg = this._serverCfg!;
|
||||||
let sp: any;
|
let sp: any;
|
||||||
const drawAspect = d.orientation === 'portrait' ? 0.707 : d.orientation === 'square' ? 1 : 1.414;
|
|
||||||
if (d.mode === 'create') {
|
if (d.mode === 'create') {
|
||||||
sp = {
|
sp = {
|
||||||
id: 's' + Date.now().toString(36),
|
id: spaceId,
|
||||||
title: d.title.trim(),
|
title: d.title.trim(),
|
||||||
plan_url: null,
|
plan_url: null,
|
||||||
aspect: d.source === 'draw' ? drawAspect : 1.414,
|
aspect: d.source === 'draw' ? drawAspect : 1.414,
|
||||||
@@ -2983,15 +3001,13 @@ class HouseplanCard extends LitElement {
|
|||||||
};
|
};
|
||||||
cfg.spaces.push(sp);
|
cfg.spaces.push(sp);
|
||||||
} else {
|
} else {
|
||||||
sp = cfg.spaces.find((x: any) => x.id === d.spaceId);
|
sp = cfg.spaces.find((x: any) => x.id === spaceId);
|
||||||
|
if (!sp) throw new Error('space ' + spaceId + ' is gone from the config');
|
||||||
sp.title = d.title.trim();
|
sp.title = d.title.trim();
|
||||||
}
|
}
|
||||||
if (d.source === 'file' && d.planFile) {
|
if (uploaded) {
|
||||||
const resp = await this.hass.callWS({
|
sp.plan_url = uploaded.url;
|
||||||
type: 'houseplan/plan/set', space_id: sp.id, ext: d.planFile.ext, data: d.planFile.b64,
|
sp.aspect = uploaded.aspect;
|
||||||
});
|
|
||||||
sp.plan_url = resp.url;
|
|
||||||
sp.aspect = d.planFile.aspect;
|
|
||||||
}
|
}
|
||||||
// switching an existing space to "draw" detaches its background image
|
// switching an existing space to "draw" detaches its background image
|
||||||
// (the uploaded file stays on disk; only the reference is cleared)
|
// (the uploaded file stays on disk; only the reference is cleared)
|
||||||
@@ -3071,14 +3087,23 @@ class HouseplanCard extends LitElement {
|
|||||||
private async _saveConfigNow(): Promise<void> {
|
private async _saveConfigNow(): Promise<void> {
|
||||||
this._dropLegacySegments();
|
this._dropLegacySegments();
|
||||||
this._cfgEpoch++;
|
this._cfgEpoch++;
|
||||||
|
// same flag the debounced writer uses: while it is set, an incoming
|
||||||
|
// `houseplan_config_updated` defers its reload instead of replacing the
|
||||||
|
// config under an unfinished write (audit L2, extended to this path)
|
||||||
|
this._cfgWriting = true;
|
||||||
try {
|
try {
|
||||||
const r = await this.hass.callWS({
|
const r = await this.hass.callWS({
|
||||||
type: 'houseplan/config/set', config: this._serverCfg, expected_rev: this._cfgRev,
|
type: 'houseplan/config/set', config: this._serverCfg, expected_rev: this._cfgRev,
|
||||||
});
|
});
|
||||||
this._cfgRev = r?.rev ?? this._cfgRev + 1;
|
this._cfgRev = r?.rev ?? this._cfgRev + 1;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.code === 'conflict') await this._reloadConfigOnly();
|
if (e?.code === 'conflict') {
|
||||||
|
this._cfgWriting = false;
|
||||||
|
await this._reloadConfigOnly();
|
||||||
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
this._cfgWriting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user