mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
committed by
claude[bot]
parent
6b5158003a
commit
71bbc95309
@@ -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": []
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -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);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+19
-19
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+3
-3
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)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
Vendored
+56
-56
@@ -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": []
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -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);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+19
-19
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -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 — позиция восстановлена, а
|
||||
повтор остаётся возможен. Остальные утверждения смока не меняются.
|
||||
|
||||
## План автотестов
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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 = '';
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user