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:
+36
-11
@@ -32,7 +32,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
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_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||
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;
|
||||
this._spaceDialog = { ...d, busy: true };
|
||||
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!;
|
||||
let sp: any;
|
||||
const drawAspect = d.orientation === 'portrait' ? 0.707 : d.orientation === 'square' ? 1 : 1.414;
|
||||
if (d.mode === 'create') {
|
||||
sp = {
|
||||
id: 's' + Date.now().toString(36),
|
||||
id: spaceId,
|
||||
title: d.title.trim(),
|
||||
plan_url: null,
|
||||
aspect: d.source === 'draw' ? drawAspect : 1.414,
|
||||
@@ -2983,15 +3001,13 @@ class HouseplanCard extends LitElement {
|
||||
};
|
||||
cfg.spaces.push(sp);
|
||||
} 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();
|
||||
}
|
||||
if (d.source === 'file' && d.planFile) {
|
||||
const resp = await this.hass.callWS({
|
||||
type: 'houseplan/plan/set', space_id: sp.id, ext: d.planFile.ext, data: d.planFile.b64,
|
||||
});
|
||||
sp.plan_url = resp.url;
|
||||
sp.aspect = d.planFile.aspect;
|
||||
if (uploaded) {
|
||||
sp.plan_url = uploaded.url;
|
||||
sp.aspect = uploaded.aspect;
|
||||
}
|
||||
// switching an existing space to "draw" detaches its background image
|
||||
// (the uploaded file stays on disk; only the reference is cleared)
|
||||
@@ -3071,14 +3087,23 @@ class HouseplanCard extends LitElement {
|
||||
private async _saveConfigNow(): Promise<void> {
|
||||
this._dropLegacySegments();
|
||||
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 {
|
||||
const r = await this.hass.callWS({
|
||||
type: 'houseplan/config/set', config: this._serverCfg, expected_rev: this._cfgRev,
|
||||
});
|
||||
this._cfgRev = r?.rev ?? this._cfgRev + 1;
|
||||
} catch (e: any) {
|
||||
if (e?.code === 'conflict') await this._reloadConfigOnly();
|
||||
if (e?.code === 'conflict') {
|
||||
this._cfgWriting = false;
|
||||
await this._reloadConfigOnly();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
this._cfgWriting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user