fix: preserve area relocation state

Issue: #403
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-01 14:53:37 +00:00
committed by claude[bot]
parent 6b5158003a
commit 71bbc95309
28 changed files with 550 additions and 190 deletions
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "ffd36ef35f81a27598b236218bc3ea18d4b46ed77613c5fcb3197c5104fc232b",
"fingerprint": "bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js",
"houseplan-assets/houseplan-card-cmbllkdu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 287386,
"initialViewGzipBytes": 287575,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/de-Drq02WaY.js",
"houseplan-assets/editor-BOsi41KI.js",
"houseplan-assets/fr-Cul7kCHo.js",
"houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js",
"houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/de-C9HvYpJv.js",
"houseplan-assets/editor-CyKPZvNS.js",
"houseplan-assets/fr-BjAKtQwB.js",
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
],
"lazyGzipBytes": 199836,
"lazyGzipBytes": 199808,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/editor-BOsi41KI.js",
"houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/editor-CyKPZvNS.js",
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js"
],
"lazyEditorGzipBytes": 144691,
"lazyEditorGzipBytes": 144676,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
],
"lazyOnboardingGzipBytes": 14034,
"lazyOnboardingGzipBytes": 14024,
"lazyLocaleFiles": [
"houseplan-assets/de-Drq02WaY.js",
"houseplan-assets/fr-Cul7kCHo.js"
"houseplan-assets/de-C9HvYpJv.js",
"houseplan-assets/fr-BjAKtQwB.js"
],
"lazyLocaleGzipBytes": 48184,
"lazyLocaleGzipBytes": 48176,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BmALn_WC.js",
"sha256": "4dc555f222d405b87ad4ae04f3dc07126e48195c66f8eac9465a6bb3e771804c",
"path": "houseplan-assets/backdrop-pick-D66Ner2L.js",
"sha256": "6f8e893fe459b025f2f89ef3cf48afd07be82699a34489d446e6d2bbea1e0f44",
"rawBytes": 20636,
"gzipBytes": 7073,
"gzipBytes": 7068,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Drq02WaY.js",
"sha256": "359a18eef16eab9a5856018014ac7b95a51d03a8c4ce3dc5fdcf1eb327f0e7a7",
"path": "houseplan-assets/de-C9HvYpJv.js",
"sha256": "c48bad47855be263551852c9e4350e6148c3bc0206ddd63136982d4414110c48",
"rawBytes": 81096,
"gzipBytes": 24346,
"gzipBytes": 24342,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-BOsi41KI.js",
"sha256": "dc2cc78e84f8114cfd84adf29b45619a1d6e226d72c6831fbbeff722a25d81fc",
"path": "houseplan-assets/editor-CyKPZvNS.js",
"sha256": "f285f0141083e5ea3bb7069126474c1e9cfb7e9073f58cb8fed8963cc131848a",
"rawBytes": 3826,
"gzipBytes": 1582,
"gzipBytes": 1578,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-Cul7kCHo.js",
"sha256": "9e32a98d1f402f1fb02e62744d910c962c9e3d88b58bbe7795b08cbc53ec6285",
"path": "houseplan-assets/fr-BjAKtQwB.js",
"sha256": "b443ec61052a852b1f7d1982b402ee3cf85f9600382dd317b55caa0543da39e2",
"rawBytes": 83283,
"gzipBytes": 23838,
"gzipBytes": 23834,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-gMOWmAJQ.js",
"sha256": "deb12ad2f9171e39418fa0aa90ae1f7e5509df3d7ac8ad8031914171ea4c817b",
"rawBytes": 1019048,
"gzipBytes": 286589,
"path": "houseplan-assets/houseplan-card-cmbllkdu.js",
"sha256": "1ac69d06e6d8717d0daa9634bb0ce911e4fded918904b06c34ac8290b3842d75",
"rawBytes": 1019911,
"gzipBytes": 286781,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Drq02WaY.js",
"houseplan-assets/editor-BOsi41KI.js",
"houseplan-assets/fr-Cul7kCHo.js",
"houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js",
"houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js"
"houseplan-assets/de-C9HvYpJv.js",
"houseplan-assets/editor-CyKPZvNS.js",
"houseplan-assets/fr-BjAKtQwB.js",
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js",
"sha256": "7a55d9593731d6feaf4a8ca11eb8b3d8a7eaf1c9760aa97f719002dbb5707d01",
"path": "houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
"sha256": "01529edd319c84ce373d6086db952d6bdcad27d303c96c0dadb717bf16f9306e",
"rawBytes": 527184,
"gzipBytes": 136036,
"gzipBytes": 136030,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js",
"sha256": "d55a2c25eed774e5af1db32e9a0b31e32eec9713eff91dc77156e8929f7d0042",
"path": "houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js",
"sha256": "67bbf22592e7fcb3060012f104c0ff3ebe2b6ea29fa771e650d2fae8b4085ccd",
"rawBytes": 28088,
"gzipBytes": 6961,
"gzipBytes": 6956,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "8329cbc9b7807d654eb773c7db7f5b63fd88ec99f6c3d00edcb1c880add37958",
"sha256": "46e873cfe52720c02c77556c3e39ec7f96b7171f7be332c59116216b49009691",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 794,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ffd36ef35f81a27598b236218bc3ea18d4b46ed77613c5fcb3197c5104fc232b";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-gMOWmAJQ.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__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-cmbllkdu.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__="ffd36ef35f81a27598b236218bc3ea18d4b46ed77613c5fcb3197c5104fc232b";try{await import("./houseplan-assets/houseplan-card-gMOWmAJQ.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__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";try{await import("./houseplan-assets/houseplan-card-cmbllkdu.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)}
+9 -4
View File
@@ -105,15 +105,20 @@ const res = await page.evaluate(async () => {
},
},
};
c._layout = { ...c._layout, d_kettle: { s: 'f1', x: 0.72, y: 0.15 } };
const kettlePoint = { s: 'f1', x: 0.72, y: 0.15 };
c._layout = { ...c._layout, d_kettle: kettlePoint };
c._regSignature = '';
c._maybeRebuildDevices();
rejectKettleRelocation = true;
window.__setRegistryArea('device', 'd_kettle', 'living_room');
await wait(500);
const failedConfigRetryable = !c._layout.d_kettle
const failedConfigRestoredAndRetryable = c._layout.d_kettle?.s === kettlePoint.s
&& c._layout.d_kettle?.x === kettlePoint.x
&& c._layout.d_kettle?.y === kettlePoint.y
&& c._serverCfg.settings.marker_area_snapshot?.d_kettle?.area === 'kitchen'
&& c._areaRelocationSyncKey === '';
&& c._areaRelocationSyncKey === ''
&& calls.some((message) => message.type === 'houseplan/layout/update'
&& message.device_id === 'd_kettle');
window.__setRegistryArea('device', 'd_kettle', 'living_room');
await wait(500);
await paint();
@@ -326,7 +331,7 @@ const res = await page.evaluate(async () => {
roomDraftRefreshed,
noStaleExplicitOverride: !!savedLamp
&& savedLamp.area === undefined && savedLamp.room_id === undefined,
failedConfigRetryable,
failedConfigRestoredAndRetryable,
configRetrySucceeded,
crossSpaceRelocated: crossSpaceDevice?.space === 'f1'
&& crossSpaceDevice?.area === 'living_room'
+239
View File
@@ -0,0 +1,239 @@
// #403: a rejected Area-provenance write restores deleted manual placement,
// while relocation invalidates only history commands owned by that device.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 1000, height: 850 }, 1);
const res = await page.evaluate(async () => {
const c = window.__card;
const wait = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
const paint = async () => {
c.requestUpdate();
await c.updateComplete;
};
const settleRelocation = async () => {
await c._areaRelocationWrite.catch(() => undefined);
await wait(80);
await paint();
};
const samePlacement = (left, right) => !!left && !!right
&& left.s === right.s && left.x === right.x && left.y === right.y;
let serverConfig = structuredClone(c._serverCfg);
let serverLayout = structuredClone(c._layout);
let configRev = Math.max(30, Number(c._cfgRev) || 0);
let layoutRev = Math.max(40, Number(c._layoutRev) || 0);
const calls = [];
let rejectConfig = null;
let rejectRestoreId = null;
let delayConfig = null;
let releaseDelayedConfig = null;
const baseCallWS = c.hass.callWS;
c.hass = { ...c.hass, callWS: async (message) => {
if (message.type === 'houseplan/config/get') {
calls.push({ type: 'config/get' });
return { config: structuredClone(serverConfig), rev: configRev, can_write: true };
}
if (message.type === 'houseplan/layout/get') {
return { layout: structuredClone(serverLayout), rev: layoutRev };
}
if (message.type === 'houseplan/layout/delete') {
calls.push({ type: 'layout/delete', id: message.device_id });
delete serverLayout[message.device_id];
return { ok: true, rev: ++layoutRev };
}
if (message.type === 'houseplan/layout/update') {
const row = { type: 'layout/update', id: message.device_id };
calls.push(row);
if (rejectRestoreId === message.device_id) {
rejectRestoreId = null;
row.result = 'rejected';
throw new Error('synthetic placement restore failure');
}
serverLayout[message.device_id] = structuredClone(message.pos);
return { ok: true, rev: ++layoutRev };
}
if (message.type === 'houseplan/config/set') {
const row = {
type: 'config/set',
snapshot: structuredClone(message.config?.settings?.marker_area_snapshot || {}),
attention: [...(message.config?.settings?.new_device_ids || [])],
};
calls.push(row);
if (rejectConfig
&& row.snapshot[rejectConfig.id]?.area === rejectConfig.area) {
const rejection = rejectConfig;
rejectConfig = null;
row.result = 'rejected';
const error = new Error('synthetic config failure');
if (rejection.code) error.code = rejection.code;
throw error;
}
if (delayConfig && row.snapshot[delayConfig.id]?.area === delayConfig.area) {
const delayed = delayConfig;
delayConfig = null;
row.result = 'delayed';
await new Promise((resolve) => { releaseDelayedConfig = resolve; });
row.result = 'accepted';
if (delayed.id === '') throw new Error('unreachable delayed fixture');
}
serverConfig = structuredClone(message.config);
return { ok: true, rev: ++configRev };
}
return baseCallWS(message);
} };
c._serverStorage = true;
c._serverCanWrite = true;
c._layoutRev = layoutRev;
c._cfgRev = configRev;
const installScenario = async ({ id, oldArea, point }) => {
await settleRelocation();
window.__setRegistryArea('device', id, oldArea);
await wait(120);
c._serverCfg = {
...c._serverCfg,
settings: {
...c._serverCfg.settings,
marker_area_snapshot: {
...(c._serverCfg.settings.marker_area_snapshot || {}),
[id]: { binding: `device:${id}`, area: oldArea },
},
new_device_ids: (c._serverCfg.settings.new_device_ids || [])
.filter((candidate) => candidate !== id),
},
};
c._layout = { ...c._layout, [id]: structuredClone(point) };
serverConfig = structuredClone(c._serverCfg);
serverLayout = structuredClone(c._layout);
c._cfgContentFingerprint = '';
c._layoutContentFingerprint = '';
c._areaRelocationSyncKey = '';
c._regSignature = '';
c._maybeRebuildDevices();
await settleRelocation();
};
const pushMove = (id, name, fromX, toX) => {
const device = c._devices.find((candidate) => candidate.id === id);
if (!device) return false;
c._devicePositionHistory.push({
name,
before: { deviceId: id, spaceId: device.space,
placement: { s: device.space, x: fromX, y: 0.31 } },
after: { deviceId: id, spaceId: device.space,
placement: { s: device.space, x: toX, y: 0.31 } },
});
return true;
};
const stableId = 'd_lamp';
const relocatedId = 'd_light1';
const original = { s: 'f1', x: 0.22, y: 0.22 };
await installScenario({ id: relocatedId, oldArea: 'living_room', point: original });
c._devicePositionHistory.clear();
const historyFixtureReady = pushMove(stableId, 'Move lamp', 0.28, 0.34)
&& pushMove(relocatedId, 'Move relocating light', 0.22, 0.27);
rejectConfig = { id: relocatedId, area: 'kitchen', code: null };
window.__setRegistryArea('device', relocatedId, 'kitchen');
await settleRelocation();
const rejectedConfigIndex = calls.findIndex((row) => row.type === 'config/set'
&& row.result === 'rejected' && row.snapshot[relocatedId]?.area === 'kitchen');
const restoreIndex = calls.findIndex((row, index) => index > rejectedConfigIndex
&& row.type === 'layout/update' && row.id === relocatedId);
const restoredLocally = samePlacement(c._layout[relocatedId], original);
const restoredOnServer = samePlacement(serverLayout[relocatedId], original);
const unrelatedHistorySurvives = historyFixtureReady
&& c._devicePositionHistory.size === 1
&& c._devicePositionHistory.undoName === 'Move lamp';
const relocatedHistoryRemoved = c._devicePositionHistory.undoName !== 'Move relocating light'
&& !c._devicePositionHistory.canRedo;
const beforeUndoRelocated = structuredClone(c._layout[relocatedId]);
await c._runDevicePositionHistory('undo');
const unrelatedUndoStillWorks = c._devicePositionHistory.canRedo
&& samePlacement(c._layout[relocatedId], beforeUndoRelocated);
// Retry with a delayed config response. Between delete and acknowledgement
// the old point must not reappear; after release the ordinary #126 result
// (advanced snapshot + attention) must complete.
delayConfig = { id: relocatedId, area: 'kitchen' };
c._regSignature = '';
c._maybeRebuildDevices();
for (let attempt = 0; attempt < 80 && !releaseDelayedConfig; attempt += 1) await wait(10);
await paint();
const movedDuringDelayedWrite = c._devices.find((device) => device.id === relocatedId);
const stalePointSuppressedWhilePending = !!releaseDelayedConfig
&& !c._layout[relocatedId]
&& movedDuringDelayedWrite?.area === 'kitchen';
releaseDelayedConfig?.();
releaseDelayedConfig = null;
await settleRelocation();
const successfulRetry = !serverLayout[relocatedId]
&& c._serverCfg.settings.marker_area_snapshot?.[relocatedId]?.area === 'kitchen'
&& c._serverCfg.settings.new_device_ids?.includes(relocatedId);
// The conflict branch must restore before it reloads and retries. Call
// ordering is the durable witness even though the retry then deletes the
// restored point again and completes normally.
const conflictId = 'd_kettle';
const conflictPoint = { s: 'f1', x: 0.72, y: 0.15 };
await installScenario({ id: conflictId, oldArea: 'kitchen', point: conflictPoint });
c._devicePositionHistory.clear();
pushMove(stableId, 'Move lamp before conflict', 0.30, 0.36);
const conflictStart = calls.length;
rejectConfig = { id: conflictId, area: 'living_room', code: 'conflict' };
window.__setRegistryArea('device', conflictId, 'living_room');
await settleRelocation();
const conflictRows = calls.slice(conflictStart);
const conflictReject = conflictRows.findIndex((row) => row.type === 'config/set'
&& row.result === 'rejected' && row.snapshot[conflictId]?.area === 'living_room');
const conflictRestore = conflictRows.findIndex((row, index) => index > conflictReject
&& row.type === 'layout/update' && row.id === conflictId);
const conflictReload = conflictRows.findIndex((row, index) => index > conflictRestore
&& row.type === 'config/get');
const conflictRetryDelete = conflictRows.findIndex((row, index) => index > conflictReload
&& row.type === 'layout/delete' && row.id === conflictId);
const conflictRestoresBeforeRetry = conflictReject >= 0
&& conflictRestore > conflictReject
&& conflictReload > conflictRestore
&& conflictRetryDelete > conflictReload;
const conflictEventuallyCompletes = !serverLayout[conflictId]
&& c._serverCfg.settings.marker_area_snapshot?.[conflictId]?.area === 'living_room'
&& c._serverCfg.settings.new_device_ids?.includes(conflictId);
const conflictKeepsUnrelatedHistory = c._devicePositionHistory.undoName
=== 'Move lamp before conflict';
// If even layout/update fails, the existing attention channel is persisted
// instead of losing the manual point silently.
const fallbackId = 'd_motion';
const fallbackPoint = { s: 'f1', x: 0.40, y: 0.75 };
await installScenario({ id: fallbackId, oldArea: 'living_room', point: fallbackPoint });
rejectConfig = { id: fallbackId, area: 'kitchen', code: null };
rejectRestoreId = fallbackId;
window.__setRegistryArea('device', fallbackId, 'kitchen');
await settleRelocation();
const fallbackEl = c.renderRoot.querySelector(`.dev[data-id="${fallbackId}"]`);
const failedRestoreLeavesAttention = !serverLayout[fallbackId]
&& c._serverCfg.settings.new_device_ids?.includes(fallbackId)
&& serverConfig.settings.new_device_ids?.includes(fallbackId)
&& c._newIds.has(fallbackId)
&& !!fallbackEl?.querySelector('.newdot');
return {
historyFixtureReady,
rejectedWriteRestoresAfterDelete: rejectedConfigIndex >= 0
&& restoreIndex > rejectedConfigIndex && restoredLocally && restoredOnServer,
unrelatedHistorySurvives,
relocatedHistoryRemoved,
unrelatedUndoStillWorks,
stalePointSuppressedWhilePending,
successfulRetry,
conflictRestoresBeforeRetry,
conflictEventuallyCompletes,
conflictKeepsUnrelatedHistory,
failedRestoreLeavesAttention,
};
});
checkAll(res);
await finish(browser, res);
+56 -56
View File
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "ffd36ef35f81a27598b236218bc3ea18d4b46ed77613c5fcb3197c5104fc232b",
"fingerprint": "bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js",
"houseplan-assets/houseplan-card-cmbllkdu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 287386,
"initialViewGzipBytes": 287575,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/de-Drq02WaY.js",
"houseplan-assets/editor-BOsi41KI.js",
"houseplan-assets/fr-Cul7kCHo.js",
"houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js",
"houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/de-C9HvYpJv.js",
"houseplan-assets/editor-CyKPZvNS.js",
"houseplan-assets/fr-BjAKtQwB.js",
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
],
"lazyGzipBytes": 199836,
"lazyGzipBytes": 199808,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/editor-BOsi41KI.js",
"houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/editor-CyKPZvNS.js",
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js"
],
"lazyEditorGzipBytes": 144691,
"lazyEditorGzipBytes": 144676,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
],
"lazyOnboardingGzipBytes": 14034,
"lazyOnboardingGzipBytes": 14024,
"lazyLocaleFiles": [
"houseplan-assets/de-Drq02WaY.js",
"houseplan-assets/fr-Cul7kCHo.js"
"houseplan-assets/de-C9HvYpJv.js",
"houseplan-assets/fr-BjAKtQwB.js"
],
"lazyLocaleGzipBytes": 48184,
"lazyLocaleGzipBytes": 48176,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BmALn_WC.js",
"sha256": "4dc555f222d405b87ad4ae04f3dc07126e48195c66f8eac9465a6bb3e771804c",
"path": "houseplan-assets/backdrop-pick-D66Ner2L.js",
"sha256": "6f8e893fe459b025f2f89ef3cf48afd07be82699a34489d446e6d2bbea1e0f44",
"rawBytes": 20636,
"gzipBytes": 7073,
"gzipBytes": 7068,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Drq02WaY.js",
"sha256": "359a18eef16eab9a5856018014ac7b95a51d03a8c4ce3dc5fdcf1eb327f0e7a7",
"path": "houseplan-assets/de-C9HvYpJv.js",
"sha256": "c48bad47855be263551852c9e4350e6148c3bc0206ddd63136982d4414110c48",
"rawBytes": 81096,
"gzipBytes": 24346,
"gzipBytes": 24342,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-BOsi41KI.js",
"sha256": "dc2cc78e84f8114cfd84adf29b45619a1d6e226d72c6831fbbeff722a25d81fc",
"path": "houseplan-assets/editor-CyKPZvNS.js",
"sha256": "f285f0141083e5ea3bb7069126474c1e9cfb7e9073f58cb8fed8963cc131848a",
"rawBytes": 3826,
"gzipBytes": 1582,
"gzipBytes": 1578,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-Cul7kCHo.js",
"sha256": "9e32a98d1f402f1fb02e62744d910c962c9e3d88b58bbe7795b08cbc53ec6285",
"path": "houseplan-assets/fr-BjAKtQwB.js",
"sha256": "b443ec61052a852b1f7d1982b402ee3cf85f9600382dd317b55caa0543da39e2",
"rawBytes": 83283,
"gzipBytes": 23838,
"gzipBytes": 23834,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-gMOWmAJQ.js",
"sha256": "deb12ad2f9171e39418fa0aa90ae1f7e5509df3d7ac8ad8031914171ea4c817b",
"rawBytes": 1019048,
"gzipBytes": 286589,
"path": "houseplan-assets/houseplan-card-cmbllkdu.js",
"sha256": "1ac69d06e6d8717d0daa9634bb0ce911e4fded918904b06c34ac8290b3842d75",
"rawBytes": 1019911,
"gzipBytes": 286781,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Drq02WaY.js",
"houseplan-assets/editor-BOsi41KI.js",
"houseplan-assets/fr-Cul7kCHo.js",
"houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js",
"houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js"
"houseplan-assets/de-C9HvYpJv.js",
"houseplan-assets/editor-CyKPZvNS.js",
"houseplan-assets/fr-BjAKtQwB.js",
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-DtAVtOU4.js",
"sha256": "7a55d9593731d6feaf4a8ca11eb8b3d8a7eaf1c9760aa97f719002dbb5707d01",
"path": "houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
"sha256": "01529edd319c84ce373d6086db952d6bdcad27d303c96c0dadb717bf16f9306e",
"rawBytes": 527184,
"gzipBytes": 136036,
"gzipBytes": 136030,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BtE_1V6_.js",
"sha256": "d55a2c25eed774e5af1db32e9a0b31e32eec9713eff91dc77156e8929f7d0042",
"path": "houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js",
"sha256": "67bbf22592e7fcb3060012f104c0ff3ebe2b6ea29fa771e650d2fae8b4085ccd",
"rawBytes": 28088,
"gzipBytes": 6961,
"gzipBytes": 6956,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BmALn_WC.js",
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/backdrop-pick-D66Ner2L.js",
"houseplan-assets/houseplan-card-cmbllkdu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "8329cbc9b7807d654eb773c7db7f5b63fd88ec99f6c3d00edcb1c880add37958",
"sha256": "46e873cfe52720c02c77556c3e39ec7f96b7171f7be332c59116216b49009691",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 794,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-gMOWmAJQ.js"
"houseplan-assets/houseplan-card-cmbllkdu.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__="ffd36ef35f81a27598b236218bc3ea18d4b46ed77613c5fcb3197c5104fc232b";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-gMOWmAJQ.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__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-cmbllkdu.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__="ffd36ef35f81a27598b236218bc3ea18d4b46ed77613c5fcb3197c5104fc232b";try{await import("./houseplan-assets/houseplan-card-gMOWmAJQ.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__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";try{await import("./houseplan-assets/houseplan-card-cmbllkdu.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)}
+5 -1
View File
@@ -110,7 +110,11 @@ the same profiler available between stable promotions.
`device-area-relocation` resolver. Its pending ids override stale layout in
both interactive and hosted-static projections immediately; a writer then
deletes those layout entries before advancing bounded Area provenance in
config. Limited registry snapshots are read-only and never infer movement.
config. If that config write is rejected, every successfully deleted manual
point is restored through the layout store before a retry; a failed restore
falls back to the existing attention marker. Relocation invalidates only
Undo/Redo commands owned by the moved device. Limited registry snapshots
are read-only and never infer movement.
5. **One modal contract.** Card modals render through `hp-dialog`. In Home
Assistant it delegates surface semantics and trapping to `ha-dialog`; the
standalone demo falls back to native `<dialog>`. The wrapper owns the title,
+7
View File
@@ -2,6 +2,13 @@
## Unreleased
- A failed config write while following a device to another Home Assistant
Area no longer silently discards its manually placed position: House Plan
restores the point before retrying, or marks the marker for attention if the
restore also fails. The move now removes Undo/Redo entries only for that
marker instead of clearing position history for every device
([#403](https://github.com/Matysh/houseplan-card/issues/403)).
- Confirmations now work before the first space exists: deleting a stored plan
during onboarding asks, and answers, instead of doing nothing
([#402](https://github.com/Matysh/houseplan-card/issues/402)).
+7
View File
@@ -8,6 +8,13 @@
## Не выпущено
- Отказ записи конфига при переносе устройства в другую HA-зону больше не
стирает молча поставленную вручную позицию: House Plan восстанавливает точку
перед повтором, а если не удалось и это — отмечает маркер для внимания.
Перенос теперь удаляет Undo/Redo только этого маркера, а не очищает историю
позиций всех устройств
([#403](https://github.com/Matysh/houseplan-card/issues/403)).
- Подтверждения работают и до создания первого пространства: удаление
сохранённого плана в онбординге теперь спрашивает и отвечает, а не молчит
([#402](https://github.com/Matysh/houseplan-card/issues/402)).
+8 -5
View File
@@ -3,7 +3,7 @@
- Issue: https://github.com/Matysh/houseplan-card/issues/403
- Приоритет: P1, bug; полный трек — класс A, две находки на одной поверхности
(свежий код #126)
- Ревизия: 2 (2026-09-01; H1/L1 из SPEC-REVIEW-403-r1)
- Ревизия: 3 (2026-09-01; техническое уточнение AC7 при реализации)
## Сценарий
@@ -171,10 +171,13 @@ best-effort контракт на touch из `docs/TOUCH-SUPPORT.md` и разд
Доказательство: смок.
- **AC6**. Записи переехавшего устройства из истории удалены — Undo не вернёт
его в комнату, которой у него больше нет. Доказательство: тот же смок.
- **AC7**. Существующее поведение #126 не сломано: ручная перестановка после
переезда переживает три rebuild-тика, delete-first echo не воскрешает точку.
Доказательство: `demo/smoke_area_relocation.mjs` остаётся зелёным без
правок его утверждений.
- **AC7**. Существующее успешное поведение #126 не сломано: ручная перестановка
после переезда переживает три rebuild-тика, delete-first echo не воскрешает
точку. Доказательство: `demo/smoke_area_relocation.mjs` остаётся зелёным;
единственное прежнее утверждение `failedConfigRetryable`, которое прямо
требовало состояние исходного бага (`layout` уже удалён после отказа
`config/set`), заменяется witness нового AC1 — позиция восстановлена, а
повтор остаётся возможен. Остальные утверждения смока не меняются.
## План автотестов
+24
View File
@@ -4306,6 +4306,30 @@ const MUTANT_DEFINITIONS = [
replace: ' const rolledBack = false;',
}],
},
{
id: 'area-relocation-loses-position-on-refusal',
guard: 'npm run bundle:sync && node demo/smoke_area_relocation_safety.mjs',
because: 'a rejected provenance write must restore every manual layout point deleted '
+ 'before config/set, or leave explicit attention when restoration also fails (#403)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' await this._persistDevicePlacement(id, placement);',
replace: ' void id;\n void placement;',
}],
},
{
id: 'area-relocation-clears-whole-history',
guard: 'npm run bundle:sync && node demo/smoke_area_relocation_safety.mjs',
because: 'moving one marker to a new HA Area must invalidate only that marker\'s commands '
+ 'instead of silently erasing Undo and Redo for every other marker (#403)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' const relocating = this._areaRelocationIds;\n'
+ ' this._devicePositionHistory.removeWhere(({ before, after }) =>\n'
+ ' relocating.has(before.deviceId) || relocating.has(after.deviceId));',
replace: ' this._devicePositionHistory.clear();',
}],
},
];
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
+9
View File
@@ -48,6 +48,15 @@ export class CommandStack<T> {
return command;
}
/** Remove commands invalidated by an external change while preserving unrelated history. */
public removeWhere(predicate: (command: NamedCommand<T>) => boolean): number {
const undoSize = this._undo.length;
const redoSize = this._redo.length;
this._undo = this._undo.filter((command) => !predicate(command));
this._redo = this._redo.filter((command) => !predicate(command));
return undoSize + redoSize - this._undo.length - this._redo.length;
}
public clear(): void {
this._undo = [];
this._redo = [];
+43 -2
View File
@@ -5062,7 +5062,9 @@ export class HouseplanCard extends LitElement {
this._areaRelocationIds = new Set(areaRelocations.relocateIds);
if (this._areaRelocationIds.size) {
this._cancelDeviceDrag();
this._devicePositionHistory.clear();
const relocating = this._areaRelocationIds;
this._devicePositionHistory.removeWhere(({ before, after }) =>
relocating.has(before.deviceId) || relocating.has(after.deviceId));
const dialog = this._markerDialog;
if (dialog?.devId && !dialog.roomTouched && this._areaRelocationIds.has(dialog.devId)) {
const current = this._devices.find((device) => device.id === dialog.devId);
@@ -5171,6 +5173,7 @@ export class HouseplanCard extends LitElement {
});
this._areaRelocationIds = new Set(current.relocateIds);
const committed = new Set<string>();
const deletedPlacements = new Map<string, DevicePlacement>();
let deleteFailed = false;
for (const decision of current.decisions) {
if (!decision.relocate) continue;
@@ -5178,6 +5181,7 @@ export class HouseplanCard extends LitElement {
try {
await this._persistDevicePlacement(decision.id, null);
committed.add(decision.id);
if (before) deletedPlacements.set(decision.id, before);
this._areaRelocationIds.delete(decision.id);
} catch (error: unknown) {
deleteFailed = true;
@@ -5236,16 +5240,53 @@ export class HouseplanCard extends LitElement {
this._serverCfg = { ...this._serverCfg!, settings: restored };
this._cfgContentFingerprint = contentFingerprint(this._serverCfg);
}
// Config and layout are separate stores, but this lifecycle change is
// one user transaction. A rejected provenance write must put every
// successfully deleted manual point back before the relocation may
// retry. Keep the ids pending while restoring so the stale point
// cannot win the render against the authoritative registry Area.
for (const id of committed) this._areaRelocationIds.add(id);
const restoreFailed = new Set<string>();
for (const [id, placement] of deletedPlacements) {
try {
await this._persistDevicePlacement(id, placement);
} catch (restoreError: unknown) {
restoreFailed.add(id);
this._showToast(this._t('toast.pos_save_failed', {
err: this._errText(restoreError),
}));
}
}
this._areaRelocationSyncKey = '';
this._regSignature = '';
const code = error && typeof error === 'object' && 'code' in error
? (error as { code?: unknown }).code : undefined;
if (code === 'conflict') {
this._showToast(this._t('toast.conflict'));
void this._reloadConfigOnly(true);
await this._reloadConfigOnly(true);
} else {
this._showToast(this._t('toast.cfg_save_failed', { err: this._errText(error) }));
}
if (restoreFailed.size && this._serverCfg) {
const attentionSettings = this._settings;
const attention = [...new Set([
...(Array.isArray(attentionSettings.new_device_ids)
? attentionSettings.new_device_ids : []),
...restoreFailed,
])];
this._serverCfg = {
...this._serverCfg,
settings: { ...attentionSettings, new_device_ids: attention },
};
this._cfgEpoch++;
try {
await this._writeConfig();
} catch (attentionError: unknown) {
this._showToast(this._t('toast.cfg_save_failed', {
err: this._errText(attentionError),
}));
}
}
}
}
if (deleteFailed) this._areaRelocationSyncKey = '';
+21
View File
@@ -29,3 +29,24 @@ test('the configured history cannot be weakened below thirty commands', () => {
for (let i = 0; i < 35; i++) stack.push({ name: String(i), before: i, after: i + 1 });
assert.equal(stack.size, 30);
});
test('selective invalidation removes matching undo and redo without clearing neighbours', () => {
const stack = new CommandStack(50);
stack.push({ name: 'lamp', before: { deviceId: 'lamp' }, after: { deviceId: 'lamp' } });
stack.push({ name: 'sensor', before: { deviceId: 'sensor' }, after: { deviceId: 'sensor' } });
stack.push({ name: 'lock', before: { deviceId: 'lock' }, after: { deviceId: 'lock' } });
assert.equal(stack.undo()?.name, 'lock');
assert.equal(
stack.removeWhere(({ before }) => before.deviceId === 'sensor'),
1,
);
assert.equal(stack.undoName, 'lamp');
assert.equal(stack.redoName, 'lock');
assert.equal(stack.canUndo, true);
assert.equal(stack.canRedo, true);
assert.equal(stack.removeWhere(({ after }) => after.deviceId === 'lock'), 1);
assert.equal(stack.canRedo, false);
assert.equal(stack.undo()?.name, 'lamp');
});