mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 19:28:46 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24b2ff8127 | ||
|
|
3883352f43 | ||
|
|
f0574c1fa7 |
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "e5edb95a1f5817c9035c6418ea0f9cb86cde238466f1f5c3174fb5246662c8ec",
|
||||
"fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 291069,
|
||||
"initialViewGzipBytes": 291175,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/de-D_5HnJeM.js",
|
||||
"houseplan-assets/editor-CqQRQq4C.js",
|
||||
"houseplan-assets/fr-DpKqMyVB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
],
|
||||
"lazyGzipBytes": 215495,
|
||||
"lazyGzipBytes": 215559,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/editor-CqQRQq4C.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 159809,
|
||||
"lazyEditorGzipBytes": 159876,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14126,
|
||||
"lazyOnboardingGzipBytes": 14133,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-D_5HnJeM.js",
|
||||
"houseplan-assets/fr-DpKqMyVB.js"
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48734,
|
||||
"lazyLocaleGzipBytes": 48727,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"sha256": "b67eb5e04c63007ff8ca7ff5623a7dcae44ff3c6f3ca5528ac7f1d389f75c4ec",
|
||||
"path": "houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805",
|
||||
"rawBytes": 20996,
|
||||
"gzipBytes": 7174,
|
||||
"gzipBytes": 7177,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-D_5HnJeM.js",
|
||||
"sha256": "ad2dde1407ae850f963cf81a8c0218c0e6646a86cf3bceb61a305cd54f60dad9",
|
||||
"path": "houseplan-assets/de-B-zT3Bet.js",
|
||||
"sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1",
|
||||
"rawBytes": 81632,
|
||||
"gzipBytes": 24639,
|
||||
"gzipBytes": 24635,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CqQRQq4C.js",
|
||||
"sha256": "5007144deff010072eb0a6a6d3e5565064a60575baa173a0250cec89b1478823",
|
||||
"path": "houseplan-assets/editor-DJZS-SMT.js",
|
||||
"sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1580,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-DpKqMyVB.js",
|
||||
"sha256": "3c03580d357a96678d80b012923e7e28e989fe6a612fe0342d5d041e2d37d4f1",
|
||||
"path": "houseplan-assets/fr-DhlZhAWs.js",
|
||||
"sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d",
|
||||
"rawBytes": 83754,
|
||||
"gzipBytes": 24095,
|
||||
"gzipBytes": 24092,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-o7i25aVl.js",
|
||||
"sha256": "9b3942532fe83c5d49791859c419fb6f35a8db86c01a09ddccb1a2ec186bfbd3",
|
||||
"rawBytes": 1033268,
|
||||
"gzipBytes": 290274,
|
||||
"path": "houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb",
|
||||
"rawBytes": 1033646,
|
||||
"gzipBytes": 290380,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-D_5HnJeM.js",
|
||||
"houseplan-assets/editor-CqQRQq4C.js",
|
||||
"houseplan-assets/fr-DpKqMyVB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js"
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js",
|
||||
"sha256": "b2a69132a223d6c5b06b8d4a11b19416d33582bae2d39b2448ecc41f44ba98b2",
|
||||
"rawBytes": 576032,
|
||||
"gzipBytes": 151055,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65",
|
||||
"rawBytes": 576162,
|
||||
"gzipBytes": 151119,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js",
|
||||
"sha256": "1324b8003d727c99c6ce6d9e48598032b29782f8c8c7b783c1b1073e9b79d9f2",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js",
|
||||
"sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6952,
|
||||
"gzipBytes": 6956,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "d6c8be124ce907703e76f24a5e9fedb3abd945213d7befb1f97d1635d82f4830",
|
||||
"sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 795,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.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__="e5edb95a1f5817c9035c6418ea0f9cb86cde238466f1f5c3174fb5246662c8ec";import{b as e,l as o,t,dg as s,A as a,dh as i,di as l,dj as n,E as r,c}from"./houseplan-card-o7i25aVl.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
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";import{b as e,l as o,t,di as s,A as a,dj as i,dk as l,dl as n,E as r,c}from"./houseplan-card-D9kogu9G.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.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(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);
|
||||
${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);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+18
-18
File diff suppressed because one or more lines are too long
+74
-74
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__="e5edb95a1f5817c9035c6418ea0f9cb86cde238466f1f5c3174fb5246662c8ec";try{await import("./houseplan-assets/houseplan-card-o7i25aVl.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__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";try{await import("./houseplan-assets/houseplan-card-D9kogu9G.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -26,6 +26,68 @@ const result = await page.evaluate(async () => {
|
||||
await card.updateComplete;
|
||||
out.defaultRoomTip = card._tip?.room === true && !!root().querySelector('.tip');
|
||||
|
||||
const originalCallWS = card.hass.callWS;
|
||||
const beforeRejectedSave = structuredClone(card._serverCfg);
|
||||
const beforeRejectedFingerprint = card._cfgContentFingerprint;
|
||||
card._openSettingsDialog();
|
||||
card._settingsDialog = {
|
||||
...card._settingsDialog,
|
||||
colors: {
|
||||
...card._settingsDialog.colors,
|
||||
light_on: { c: '#123456', a: 0.4 },
|
||||
},
|
||||
glowRadius: 4,
|
||||
bgColor: '#234567',
|
||||
northDeg: 45,
|
||||
bgMode: 'static',
|
||||
sunRays: true,
|
||||
showRoomTooltip: false,
|
||||
};
|
||||
card.hass = { ...card.hass, callWS: async (message) => {
|
||||
if (message.type === 'houseplan/config/set') throw new Error('offline');
|
||||
return originalCallWS(message);
|
||||
} };
|
||||
await card._saveSettingsDialog();
|
||||
await card.updateComplete;
|
||||
out.rejectedSettingsRolledBack = JSON.stringify(card._serverCfg)
|
||||
=== JSON.stringify(beforeRejectedSave);
|
||||
out.rejectedFingerprintRolledBack = card._cfgContentFingerprint
|
||||
=== beforeRejectedFingerprint;
|
||||
out.rejectedDraftKept = card._settingsDialog?.showRoomTooltip === false
|
||||
&& card._settingsDialog?.bgColor === '#234567'
|
||||
&& card._settingsDialog?.colors.light_on.c === '#123456';
|
||||
out.rejectedBusyCleared = card._settingsDialog?.busy === false;
|
||||
card._tip = null;
|
||||
room().dispatchEvent(mouse('pointermove', 185, 185));
|
||||
await card.updateComplete;
|
||||
out.rejectedRuntimeUsesServerSettings = card._tip?.room === true;
|
||||
|
||||
const authoritative = structuredClone(beforeRejectedSave);
|
||||
authoritative.settings = { ...authoritative.settings, bg_color: '#345678' };
|
||||
const authoritativeRev = card._cfgRev + 5;
|
||||
card._openSettingsDialog();
|
||||
card._settingsDialog = {
|
||||
...card._settingsDialog, bgColor: '#abcdef', showRoomTooltip: false,
|
||||
};
|
||||
card.hass = { ...card.hass, callWS: async (message) => {
|
||||
if (message.type === 'houseplan/config/set') {
|
||||
throw Object.assign(new Error('conflict'), { code: 'conflict' });
|
||||
}
|
||||
if (message.type === 'houseplan/config/get') {
|
||||
return { config: structuredClone(authoritative), rev: authoritativeRev, can_write: true };
|
||||
}
|
||||
return originalCallWS(message);
|
||||
} };
|
||||
await card._saveSettingsDialog();
|
||||
await card.updateComplete;
|
||||
out.conflictKeepsAuthoritative = card._serverCfg.settings.bg_color === '#345678'
|
||||
&& !Object.hasOwn(card._serverCfg.settings, 'show_room_tooltip')
|
||||
&& card._cfgRev === authoritativeRev;
|
||||
out.conflictDraftKept = card._settingsDialog?.bgColor === '#abcdef'
|
||||
&& card._settingsDialog?.showRoomTooltip === false
|
||||
&& card._settingsDialog?.busy === false;
|
||||
card.hass = { ...card.hass, callWS: originalCallWS };
|
||||
|
||||
card._openSettingsDialog();
|
||||
card._settingsDialog = { ...card._settingsDialog, showRoomTooltip: false };
|
||||
await card._saveSettingsDialog();
|
||||
@@ -70,6 +132,13 @@ checkAll(result, {
|
||||
defaultOn: true,
|
||||
cancelKeepsAbsentDefault: true,
|
||||
defaultRoomTip: true,
|
||||
rejectedSettingsRolledBack: true,
|
||||
rejectedFingerprintRolledBack: true,
|
||||
rejectedDraftKept: true,
|
||||
rejectedBusyCleared: true,
|
||||
rejectedRuntimeUsesServerSettings: true,
|
||||
conflictKeepsAuthoritative: true,
|
||||
conflictDraftKept: true,
|
||||
falsePersists: true,
|
||||
visibleRoomTipCleared: true,
|
||||
disabledRoomTip: true,
|
||||
|
||||
Vendored
+55
-55
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "e5edb95a1f5817c9035c6418ea0f9cb86cde238466f1f5c3174fb5246662c8ec",
|
||||
"fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 291069,
|
||||
"initialViewGzipBytes": 291175,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/de-D_5HnJeM.js",
|
||||
"houseplan-assets/editor-CqQRQq4C.js",
|
||||
"houseplan-assets/fr-DpKqMyVB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
],
|
||||
"lazyGzipBytes": 215495,
|
||||
"lazyGzipBytes": 215559,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/editor-CqQRQq4C.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 159809,
|
||||
"lazyEditorGzipBytes": 159876,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14126,
|
||||
"lazyOnboardingGzipBytes": 14133,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-D_5HnJeM.js",
|
||||
"houseplan-assets/fr-DpKqMyVB.js"
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48734,
|
||||
"lazyLocaleGzipBytes": 48727,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"sha256": "b67eb5e04c63007ff8ca7ff5623a7dcae44ff3c6f3ca5528ac7f1d389f75c4ec",
|
||||
"path": "houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805",
|
||||
"rawBytes": 20996,
|
||||
"gzipBytes": 7174,
|
||||
"gzipBytes": 7177,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-D_5HnJeM.js",
|
||||
"sha256": "ad2dde1407ae850f963cf81a8c0218c0e6646a86cf3bceb61a305cd54f60dad9",
|
||||
"path": "houseplan-assets/de-B-zT3Bet.js",
|
||||
"sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1",
|
||||
"rawBytes": 81632,
|
||||
"gzipBytes": 24639,
|
||||
"gzipBytes": 24635,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CqQRQq4C.js",
|
||||
"sha256": "5007144deff010072eb0a6a6d3e5565064a60575baa173a0250cec89b1478823",
|
||||
"path": "houseplan-assets/editor-DJZS-SMT.js",
|
||||
"sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1580,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-DpKqMyVB.js",
|
||||
"sha256": "3c03580d357a96678d80b012923e7e28e989fe6a612fe0342d5d041e2d37d4f1",
|
||||
"path": "houseplan-assets/fr-DhlZhAWs.js",
|
||||
"sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d",
|
||||
"rawBytes": 83754,
|
||||
"gzipBytes": 24095,
|
||||
"gzipBytes": 24092,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-o7i25aVl.js",
|
||||
"sha256": "9b3942532fe83c5d49791859c419fb6f35a8db86c01a09ddccb1a2ec186bfbd3",
|
||||
"rawBytes": 1033268,
|
||||
"gzipBytes": 290274,
|
||||
"path": "houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb",
|
||||
"rawBytes": 1033646,
|
||||
"gzipBytes": 290380,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-D_5HnJeM.js",
|
||||
"houseplan-assets/editor-CqQRQq4C.js",
|
||||
"houseplan-assets/fr-DpKqMyVB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js"
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-D0JYjEs4.js",
|
||||
"sha256": "b2a69132a223d6c5b06b8d4a11b19416d33582bae2d39b2448ecc41f44ba98b2",
|
||||
"rawBytes": 576032,
|
||||
"gzipBytes": 151055,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65",
|
||||
"rawBytes": 576162,
|
||||
"gzipBytes": 151119,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-C-SJxeJ_.js",
|
||||
"sha256": "1324b8003d727c99c6ce6d9e48598032b29782f8c8c7b783c1b1073e9b79d9f2",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js",
|
||||
"sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6952,
|
||||
"gzipBytes": 6956,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-Dg2Uytel.js",
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "d6c8be124ce907703e76f24a5e9fedb3abd945213d7befb1f97d1635d82f4830",
|
||||
"sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 795,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-o7i25aVl.js"
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.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__="e5edb95a1f5817c9035c6418ea0f9cb86cde238466f1f5c3174fb5246662c8ec";import{b as e,l as o,t,dg as s,A as a,dh as i,di as l,dj as n,E as r,c}from"./houseplan-card-o7i25aVl.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
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";import{b as e,l as o,t,di as s,A as a,dj as i,dk as l,dl as n,E as r,c}from"./houseplan-card-D9kogu9G.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.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(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);
|
||||
${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);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+18
-18
File diff suppressed because one or more lines are too long
+74
-74
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__="e5edb95a1f5817c9035c6418ea0f9cb86cde238466f1f5c3174fb5246662c8ec";try{await import("./houseplan-assets/houseplan-card-o7i25aVl.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__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";try{await import("./houseplan-assets/houseplan-card-D9kogu9G.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- If Home Assistant rejects a General settings save, the plan now returns to
|
||||
the last server-confirmed state instead of displaying an unsaved local draft;
|
||||
the entered values remain in the open dialog for another attempt
|
||||
([#439](https://github.com/Matysh/houseplan-card/issues/439)).
|
||||
|
||||
## v1.71.0-beta.2 — 2026-09-03
|
||||
|
||||
- Custom background images now recover cleanly after interrupted uploads,
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Если Home Assistant отклоняет сохранение общих настроек, план снова показывает
|
||||
последнее подтверждённое сервером состояние вместо несохранённого локального
|
||||
черновика; введённые значения остаются в открытом диалоге для повторной
|
||||
попытки ([#439](https://github.com/Matysh/houseplan-card/issues/439)).
|
||||
|
||||
## v1.71.0-beta.2 — 2026-09-03
|
||||
|
||||
- Пользовательские картинки подложки теперь корректно восстанавливаются после
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": "oxipng 10.2.0",
|
||||
"sourceFingerprint": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceFingerprint": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"captureScriptSha256": "cadb8e1bcab9f1dcdd7d75b3b90ddcbaaeb2b8c2a098f575a21f39ff70f5c59c",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "80a70361dc18dd0461568df332062e6482c633af5d280954f8b675701418a76d"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "4106cc28847047505f46921ff95765d8abdf5b382d9d17d4c5e4ad129dd8f6be"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "617b51b3648498787b5039980c9f3eceb75ba56ed63a1a20e616bc05bc304362"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "1dab6cc3b9d1bf7d8c40f0e5137f8c688683c9b7eabc5167da99d41ecfdd5b79"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "94ef50762753c0ac6ddc84d2521c4232a3f9ecd89843810d2df61c517592bed7"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "7a601769de38aa19c2e280f6ff4cf3695b854b1d1c6d1f8de4e6b5e8f7eb1559"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "0939875f8631694f4de0ef4fd01032010ede8c7d49fb7c0a857612d4b3afff93"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "1169cf86d01e9ff27d4fd4e55f114d299eb094aa84799c395b45ad914cffc24b",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"imageSha256": "cb37f7eefd936f98ef44969b21dfe8e40ee27d1f558abe8d53c0223f6488b8a6"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# CODE-REVIEW-439-r1
|
||||
|
||||
Issue: #439 — «Отказ сохранения настроек пространства оставляет применённым то,
|
||||
чего на сервере нет»
|
||||
Ветка: `issue/439-settings-save-rollback`
|
||||
SHA на момент ревью: `3883352f` (родитель `f0574c1f`)
|
||||
Трек: `trivial` (короткий, ТЗ в теле issue). Заход r1, блокирующих циклов
|
||||
израсходовано 0 из 2.
|
||||
|
||||
## Скоуп
|
||||
|
||||
Диалог общих настроек пространства применял новый `settings` к
|
||||
`_serverCfg` **оптимистично**, до подтверждения записи сервером
|
||||
(`houseplan/config/set`). При обычном отказе записи (не `conflict`) откат не
|
||||
происходил: карточка продолжала показывать непринятые сервером настройки, а
|
||||
`show_room_tooltip` (и любая другая опция) вела себя так, будто запись
|
||||
прошла — при том что ТЗ #426 объявляло этот риск снятым.
|
||||
|
||||
Три AC короткого трека:
|
||||
- **AC1** — обычный отказ: `_serverCfg.settings` и его content fingerprint
|
||||
откатываются к снимку до нажатия «Сохранить»; диалог остаётся открыт с
|
||||
введённым draft, `busy=false`, тост ошибки.
|
||||
- **AC2** — конфликт: авторитетный `config/get` после `conflict` не
|
||||
перезаписывается локальным rollback-снимком.
|
||||
- **AC3** — успех: не регрессирует.
|
||||
|
||||
Изменения: `src/serialized-write-queue.ts` (+49, новые
|
||||
`optimisticAttempt`/`rollbackOptimistic`), `src/houseplan-editor-runtime.ts`
|
||||
(точка вызова в `_saveSettingsDialog`, ~10 строк), новый браузерный smoke
|
||||
`demo/smoke_room_tooltip_toggle.mjs` (+69), юнит-тесты на сам механизм,
|
||||
оба changelog, собранный бандл (класс D), принятый fingerprint
|
||||
документационных скриншотов.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
### Чтение кода (доказательство по AC)
|
||||
|
||||
- **AC1.** `_saveSettingsDialog` (`src/houseplan-editor-runtime.ts:10228+`)
|
||||
до оптимистичного присваивания сохраняет `attempt =
|
||||
optimisticAttempt(cfg, nextConfig, _cfgContentFingerprint, _cfgRev,
|
||||
contentFingerprint)` — глубокий клон предыдущего `_serverCfg` плюс его
|
||||
fingerprint и текущую ревизию. В `catch` вызывается `rollbackOptimistic`,
|
||||
которая переписывает `_serverCfg`/`_cfgContentFingerprint` обратно и
|
||||
дёргает `requestUpdate()`, **только если** `host._cfgRev` не сдвинулась и
|
||||
текущий `_serverCfg` либо тот же объект, либо содержательно совпадает с
|
||||
неудавшейся попыткой. `_cfgRev` в `_sendConfigCandidate`
|
||||
(`houseplan-card.ts:7677-7683`) обновляется **только после** успешного
|
||||
`callWS` — на отказе строка `this._cfgRev = response?.rev …` не
|
||||
выполняется, значит guard пропускает откат. Проверено чтением и
|
||||
подтверждено smoke (ниже).
|
||||
- **AC2.** Конфликтная ветка `_saveConfigNow`
|
||||
(`houseplan-editor-runtime.ts:8930-8941`) уже до правки перечитывала
|
||||
конфиг через `_reloadConfigOnly()` и **до** повторного throw. Ревизия из
|
||||
ответа `config/get` монотонно больше ожидавшейся (иначе `conflict` не
|
||||
возник бы), поэтому к моменту, когда внешний `catch` в
|
||||
`_saveSettingsDialog` вызывает `rollbackOptimistic`, `host._cfgRev !==
|
||||
attempt.revision` истинно и функция возвращает `false`, не трогая уже
|
||||
авторитетный `_serverCfg`. Проверено чтением и smoke.
|
||||
- **AC3.** Успешный путь `attempt` не читает вовсе — ветка `try` не
|
||||
изменена в части закрытия диалога/применения настроек. Регрессия
|
||||
подтверждена существующими и новыми smoke-проверками.
|
||||
|
||||
### Гейты, которые прогнал сам (на SHA `3883352f`, зелёного Validate на нём не найдено)
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| Тайпчек | `npx tsc --noEmit` | чисто, без вывода |
|
||||
| Юнит-тесты | `npm test` | 1814 passed, 0 failed, 1 skipped — совпадает с тем, что заявил автор |
|
||||
| Сборка + сверка 3 копий бандла | `npm run build`, затем `npm run bundle:sync` | сборка чистая; `git status --porcelain` после обеих команд пуст — `dist/`, `custom_components/.../frontend/`, `demo/srv/assets` уже в актуальном состоянии, commit не разошёлся с исходником |
|
||||
| Документационный фингерпринт | `node scripts/check-docs.mjs` (diff трогает `src/**`) | «Documentation checks passed (7 files, 10 external links)» — подтверждает принятый в `3883352f` fingerprint корректен |
|
||||
| Named smoke (AC) | `node demo/smoke_room_tooltip_toggle.mjs` | все 21 проверка `true`, `OK` |
|
||||
| Дисциплина «тест умеет падать» | временно заменил `if (attempt) rollbackOptimistic(...)` на no-op, пересобрал бандл, перезапустил тот же smoke | покраснели ровно 3 проверки: `rejectedSettingsRolledBack`, `rejectedFingerprintRolledBack`, `rejectedRuntimeUsesServerSettings` — то есть именно AC1. Дерево восстановлено (`cp` бэкапа + `bundle:sync`), `git status --porcelain` снова пуст |
|
||||
| Бюджет бандла (дёшево, для полноты) | `npm run bundle:budget` | initial View 291175 B gzip / 300000 бюджет, есть предупреждение о запасе <15000 Б (#367) — это существующее давление на бюджет всего проекта, не следствие 40 строк этого диффа; не блокирует |
|
||||
|
||||
### Выбор смоков по дельте
|
||||
|
||||
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`:
|
||||
изменено файлов `src/**`: 2, символов на изменённых строках: 9, матрица 216
|
||||
смоков (`ls demo/smoke_*.mjs | wc -l` = 216).
|
||||
|
||||
**Прямое совпадение (4)** — прогнал все:
|
||||
- `demo/smoke_room_tooltip_toggle.mjs` — назван в AC, см. выше.
|
||||
- `demo/smoke_area_relocation_safety.mjs` — зелёный, все 11 проверок `true`.
|
||||
- `demo/smoke_v8_draft_write.mjs` — зелёный, все 16 проверок `true`.
|
||||
- `demo/smoke_optimize_coordinate_canonicalization.mjs` — зелёный, все 17
|
||||
проверок `true`.
|
||||
|
||||
**Слабая связь — одно распространённое имя (20, `_cfgRev`)** — не прогонял.
|
||||
Решение: диф ограничен единственной точкой входа —
|
||||
`catch`-блоком `_saveSettingsDialog`, который не переиспользуется другими
|
||||
диалогами/потоками записи; `_cfgRev` в остальных 20 смоках фигурирует по
|
||||
несвязанным путям (drag, layout, virtual light, discovery-фильтры и т.д.),
|
||||
ни один из них не проходит через `_saveSettingsDialog`. Считаю прогон
|
||||
избыточным для этой дельты.
|
||||
|
||||
Golden/perf/backend: не прогонял — diff не меняет геометрию, стили или слои
|
||||
в успешном пути (единственное видимое отличие — поведение при уже
|
||||
подтверждённом отказе записи, вне сценариев golden-капчура); backend
|
||||
(`custom_components/**/*.py`) не тронут; perf не назван в AC. Инварианты
|
||||
модели (`npm run invariants`) не прогонял — diff не трогает рёбра комнат,
|
||||
записи толщины, `layout`, `marker.space`, `open_spans`.
|
||||
|
||||
## Находки
|
||||
|
||||
Ни одной блокирующей (High) или Medium-в-скоупе находки. Одна Low,
|
||||
снимается с записью ниже — без отдельного issue (в скоуп не выходит).
|
||||
|
||||
**Low — обещанный в самом ТЗ второй сдвиг `_cfgEpoch` не реализован.**
|
||||
Раздел «Реализация и границы» в теле issue: «Одновременно восстановить
|
||||
`_cfgContentFingerprint`, сдвинуть cache epoch и запросить render.»
|
||||
`rollbackOptimistic` (`src/serialized-write-queue.ts`) восстанавливает
|
||||
`_serverCfg` и `_cfgContentFingerprint`, дёргает `requestUpdate()`, но
|
||||
**не трогает `_cfgEpoch`** — тип `host` в её сигнатуре его даже не
|
||||
перечисляет. `_cfgEpoch` бампается один раз в начале `_saveConfigNow`
|
||||
(строка 8931) и второй раз после отката не сдвигается.
|
||||
|
||||
Разобрал, порождает ли это видимый дефект: `_cfgEpoch` участвует в ключах
|
||||
кэшей геометрии/предпросмотра (контуры комнат, объединение стен, солнечные
|
||||
лучи) и в редакторских кэшах декора — но геометрию/декор этот диалог не
|
||||
меняет, а единственный settings-зависимый кэш, ключующийся по эпохе
|
||||
(солнечные лучи, `houseplan-card.ts:10488`), включает `north`/значения
|
||||
режима **прямо в строку ключа**, а не полагается на эпоху как на
|
||||
единственный признак изменения. Поэтому даже кэш-промах после несостоявшегося
|
||||
второго бампа не может отдать неверные лучи для другого `north` — ключ
|
||||
и так другой; а для того же `north` результат совпадает по построению.
|
||||
Цвета/glow/фон читаются из `_serverCfg.settings` в рендере без отдельного
|
||||
эпоха-кэша. Видимого дефекта не нашёл ни рассуждением, ни smoke (который
|
||||
как раз проверяет наблюдаемое поведение runtime после отката —
|
||||
`rejectedRuntimeUsesServerSettings`).
|
||||
|
||||
Снимаю без правки: разрыв между текстом ТЗ и кодом реален, но не создаёт
|
||||
наблюдаемого расхождения при нынешней архитектуре кэшей. Если в будущем
|
||||
появится settings-зависимый эпоха-кэш без значения в ключе (например,
|
||||
для `bg_color`/`glow_radius_cm`), тот же паттерн уже будет тихо ломаться —
|
||||
стоит иметь в виду при следующей работе с `rollbackOptimistic`, но заводить
|
||||
это отдельным issue сейчас избыточно: одно наблюдение без текущего
|
||||
пользовательского эффекта.
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- Идентити-guard (`_cfgRev` + ссылочное/содержательное сравнение
|
||||
`_serverCfg`) корректно различает «наш неудавшийся кандидат» и
|
||||
«авторитетный конфликтный reload» / «более новую правку» — подтверждено
|
||||
и чтением кода, и двумя юнит-тестами
|
||||
(`test/serialized-write-queue.test.mjs`), и smoke AC2.
|
||||
`_cfgRev` меняется исключительно после успешного ответа сервера
|
||||
(`_sendConfigCandidate`), поэтому на обычном отказе guard всегда
|
||||
пропускает откат — структурно, а не по счастливой случайности с таймингом.
|
||||
- Глубокий клон снимка (`JSON.parse(JSON.stringify(previous))` в
|
||||
`optimisticAttempt`) — восстановленный `_serverCfg` не делит ссылку с
|
||||
последующими мутациями; проверено юнит-тестом
|
||||
(`assert.notEqual(host._serverCfg, previous, ...)`).
|
||||
- draft в диалоге не трогается веткой rollback — `_settingsDialog` в
|
||||
`catch` только теряет `busy`; введённые значения остаются, что и требует
|
||||
AC1/AC2 (smoke `rejectedDraftKept`, `conflictDraftKept`).
|
||||
- `_saveConfigNow`'s conflict-ветка (`await
|
||||
this.host._reloadConfigOnly()` до повторного throw) не переписана этим
|
||||
диффом — новый код добавлен строго в внешнем `catch`, не пересекается с
|
||||
существующей веткой rollback для физической геометрии
|
||||
(`physicalGeometryRolledBack`, `_reloadRejectedPhysicalWrite`), которая
|
||||
этот диф тоже не трогает.
|
||||
- Оба changelog в том же коммите (`f0574c1f`), формулировка соответствует
|
||||
фактическому поведению (AC1), не переобещает про AC2 (внутренняя защита,
|
||||
не новое видимое поведение) — текст `docs/CHANGELOG.md` /
|
||||
`docs/CHANGELOG.ru.md` сверен построчно.
|
||||
- Трейлеры `Issue: #439` и `User-Visible: yes|no` на обоих коммитах на
|
||||
месте; `User-Visible: yes` в `f0574c1f` сопровождается правкой обоих
|
||||
changelog в этом же коммите.
|
||||
- Классификация `trivial`: один диалог/модуль, 3 AC, ожидаемое поведение
|
||||
зафиксировано контрактом #426 — критерии §5.1 выполнены, спор не вижу.
|
||||
- Единственный источник числа/значения: диф не вводит новое отображаемое
|
||||
значение — только восстанавливает существующий источник (`_serverCfg`)
|
||||
к его прежнему состоянию; `test/single-source-numbers.test.mjs` в
|
||||
прогоне `npm test` зелёный без изменений в его области.
|
||||
|
||||
## Чего не проверял и почему
|
||||
|
||||
- Полный набор из 216 смоков и `golden:verify`/`performance_smoke` — diff
|
||||
не расширяется за пределы одного диалога, это предрелизная обязанность
|
||||
(§8), не гейт этого ревью.
|
||||
- 20 смоков со слабой связью по `_cfgRev` — см. обоснование выше.
|
||||
- `npm run invariants` — diff не касается геометрии/толщины/`layout`.
|
||||
- `python -m pytest tests_backend` — `custom_components/**/*.py` не
|
||||
тронут.
|
||||
- Ручное тестирование в браузере (вне smoke-харнесса) — не запускал,
|
||||
цикл ревью не предполагает ручной прогон; smoke дублирует ровно тот же
|
||||
DOM/hass-стенд.
|
||||
- Кросс-языковая идентичность канонизации координат (JS↔Python,
|
||||
`LATTICE_GRID_N=240`, `LATTICE_NOISE_STEPS=1e-4`) как потенциальный
|
||||
источник unstable fingerprint после `canonicalizeConfigGeometry` внутри
|
||||
`_writeConfig` — рассмотрел теоретически (если бы бэкенд не гарантировал
|
||||
фиксированную точку, guard мог бы пропустить откат для конфигов с
|
||||
«грязной» геометрией). Не нашёл сценария: backend
|
||||
(`custom_components/houseplan/websocket_api.py`) канонизирует конфиг на
|
||||
каждом `config/get`/`config/set` тем же алгоритмом, поэтому входящий
|
||||
`_serverCfg` уже канонический и повторная канонизация — no-op по
|
||||
построению. Не свежий риск этого диффа (канонизация всего конфига при
|
||||
любой записи settings существовала и до #439); не заводил как находку.
|
||||
|
||||
## Вердикт
|
||||
|
||||
Все три AC доказаны: AC1/AC3 существующим+новым smoke и юнит-тестами,
|
||||
AC2 — smoke плюс структурным чтением guard'а (ревизия монотонна только
|
||||
вверх и обновляется исключительно на успехе). Единственная находка — Low,
|
||||
снята с запиской, без наблюдаемого дефекта. Гейты, которые обязаны быть
|
||||
зелёными на этом заходе, зелёные (пересчитано мной, не только заявлено
|
||||
автором). Trivial-классификация обоснована.
|
||||
|
||||
**Вердикт: зелёный.**
|
||||
|
||||
---
|
||||
|
||||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||||
|
||||
## Материал раунда
|
||||
|
||||
- Ветка: `issue/439-settings-save-rollback`, коммит `3883352f43e7` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||||
- Дерево материала: `39c0bc135ef59643339eb2d2fbee5a52e4ad6b3f`
|
||||
```
|
||||
git log --all --format='%H %T' | grep 39c0bc135ef5
|
||||
```
|
||||
@@ -242,7 +242,7 @@ import {
|
||||
canonicalizePosition,
|
||||
formatLatticeShiftCm,
|
||||
} from './coordinate-canonicalization';
|
||||
import { enqueueSerializedWrite } from './serialized-write-queue';
|
||||
import { enqueueSerializedWrite, optimisticAttempt, rollbackOptimistic, type OptimisticAttempt } from './serialized-write-queue';
|
||||
import { hasTranslation, langOf, t, type I18nKey } from './i18n';
|
||||
import { supportT, type SupportI18nKey } from './i18n/support';
|
||||
import {
|
||||
@@ -10228,6 +10228,7 @@ public _updateDecorStyle(next: DecorStyle): void {
|
||||
const d = this.host._settingsDialog;
|
||||
if (!d || d.busy) return;
|
||||
this.host._settingsDialog = { ...d, busy: true };
|
||||
let attempt: OptimisticAttempt<ServerConfig> | null = null;
|
||||
try {
|
||||
const cfg = this.host._serverCfg!;
|
||||
const isDefault = JSON.stringify(d.colors) === JSON.stringify(DEFAULT_FILL_COLORS);
|
||||
@@ -10239,8 +10240,7 @@ public _updateDecorStyle(next: DecorStyle): void {
|
||||
else delete settings.glow_radius_cm;
|
||||
if (d.bgColor) settings.bg_color = d.bgColor;
|
||||
else delete settings.bg_color;
|
||||
// Sun background compatibility is explicit: legacy absence means static,
|
||||
// while new installations and spaces materialise daynight.
|
||||
// Legacy absence means static; new installations/spaces materialise daynight.
|
||||
if (d.northDeg !== null && Number.isInteger(d.northDeg) && d.northDeg >= 0 && d.northDeg <= 359)
|
||||
settings.north_deg = d.northDeg;
|
||||
else delete settings.north_deg;
|
||||
@@ -10249,21 +10249,22 @@ public _updateDecorStyle(next: DecorStyle): void {
|
||||
else delete settings.sun_rays;
|
||||
if (d.showRoomTooltip) delete settings.show_room_tooltip;
|
||||
else settings.show_room_tooltip = false;
|
||||
// Legacy compatibility: old configs may still contain this accepted
|
||||
// field, but weather no longer affects sunlight and the UI no longer
|
||||
// exposes it. Saving general settings cleans the obsolete value up.
|
||||
// Old configs may still contain this accepted field, but weather no
|
||||
// longer affects sunlight; saving general settings cleans it up.
|
||||
delete settings.weather_entity;
|
||||
this.host._serverCfg = { ...cfg, settings };
|
||||
const nextConfig = { ...cfg, settings };
|
||||
attempt = optimisticAttempt(cfg, nextConfig, this.host._cfgContentFingerprint,
|
||||
this.host._cfgRev, contentFingerprint);
|
||||
this.host._serverCfg = nextConfig;
|
||||
await this._saveConfigNow();
|
||||
if (!d.showRoomTooltip && this.host._tip?.room) this.host._tip = null;
|
||||
this.host._settingsDialog = null;
|
||||
this.host.requestUpdate();
|
||||
this.host._showToast(this.host._t('gs.saved'));
|
||||
} catch (e: any) {
|
||||
// audit L3: the dialog may have been closed (Esc) while the save was
|
||||
// in flight — spreading null yields a truthy husk and the renderer
|
||||
// then crashes, blanking the whole card. The toast below is the
|
||||
// only remaining signal, so it must still fire.
|
||||
// Esc may close the dialog in flight; the toast must still fire (audit L3).
|
||||
// Never overwrite an authoritative conflict reload or newer edit (#439).
|
||||
if (attempt) rollbackOptimistic(this.host, attempt, contentFingerprint);
|
||||
if (this.host._settingsDialog) this.host._settingsDialog = { ...this.host._settingsDialog, busy: false };
|
||||
this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) }));
|
||||
}
|
||||
|
||||
@@ -9,3 +9,52 @@ export function enqueueSerializedWrite(
|
||||
): Promise<void> {
|
||||
return previous.catch(() => undefined).then(write);
|
||||
}
|
||||
|
||||
export interface OptimisticAttempt<T> {
|
||||
previous: T;
|
||||
previousFingerprint: string;
|
||||
revision: number;
|
||||
attempted: T;
|
||||
attemptedFingerprint: string;
|
||||
}
|
||||
|
||||
/** Capture the server-backed value before exposing a write candidate locally. */
|
||||
export function optimisticAttempt<T>(
|
||||
previous: T,
|
||||
attempted: T,
|
||||
previousFingerprint: string,
|
||||
revision: number,
|
||||
fingerprint: (value: T) => string,
|
||||
): OptimisticAttempt<T> {
|
||||
return {
|
||||
previous: JSON.parse(JSON.stringify(previous)) as T,
|
||||
previousFingerprint,
|
||||
revision,
|
||||
attempted,
|
||||
attemptedFingerprint: fingerprint(attempted),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll back only the failed candidate represented by this attempt. A conflict
|
||||
* reload or a newer edit owns a different revision/content and must win.
|
||||
*/
|
||||
export function rollbackOptimistic<T>(
|
||||
host: {
|
||||
_serverCfg: T | null;
|
||||
_cfgRev: number;
|
||||
_cfgContentFingerprint: string;
|
||||
requestUpdate: () => unknown;
|
||||
},
|
||||
attempt: OptimisticAttempt<T>,
|
||||
fingerprint: (value: T) => string,
|
||||
): boolean {
|
||||
const current = host._serverCfg;
|
||||
if (!current || host._cfgRev !== attempt.revision
|
||||
|| (current !== attempt.attempted
|
||||
&& fingerprint(current) !== attempt.attemptedFingerprint)) return false;
|
||||
host._serverCfg = attempt.previous;
|
||||
host._cfgContentFingerprint = attempt.previousFingerprint;
|
||||
host.requestUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { enqueueSerializedWrite } from '../test-build/serialized-write-queue.js';
|
||||
import {
|
||||
enqueueSerializedWrite,
|
||||
optimisticAttempt,
|
||||
rollbackOptimistic,
|
||||
} from '../test-build/serialized-write-queue.js';
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
@@ -51,3 +55,48 @@ test('a failed write does not poison the next queued edit (#224)', async () => {
|
||||
await recovered;
|
||||
assert.deepEqual(seen, ['failed', 'recovered']);
|
||||
});
|
||||
|
||||
test('a rejected optimistic write restores only its own candidate (#439)', () => {
|
||||
const fingerprint = (value) => JSON.stringify(value);
|
||||
const previous = { settings: { enabled: true } };
|
||||
const attempted = { settings: { enabled: false } };
|
||||
const attempt = optimisticAttempt(previous, attempted, 'server-fingerprint', 7, fingerprint);
|
||||
let updates = 0;
|
||||
const host = {
|
||||
_serverCfg: attempted,
|
||||
_cfgRev: 7,
|
||||
_cfgContentFingerprint: fingerprint(attempted),
|
||||
requestUpdate: () => { updates += 1; },
|
||||
};
|
||||
|
||||
assert.equal(rollbackOptimistic(host, attempt, fingerprint), true);
|
||||
assert.deepEqual(host._serverCfg, previous);
|
||||
assert.notEqual(host._serverCfg, previous, 'the rollback snapshot is isolated from later mutations');
|
||||
assert.equal(host._cfgContentFingerprint, 'server-fingerprint');
|
||||
assert.equal(updates, 1);
|
||||
});
|
||||
|
||||
test('a conflict reload or newer mutation wins over a rejected candidate (#439)', () => {
|
||||
const fingerprint = (value) => JSON.stringify(value);
|
||||
const previous = { settings: { value: 'server-before' } };
|
||||
const attempted = { settings: { value: 'draft' } };
|
||||
const attempt = optimisticAttempt(previous, attempted, 'before', 3, fingerprint);
|
||||
const authoritative = { settings: { value: 'server-after' } };
|
||||
let updates = 0;
|
||||
const host = {
|
||||
_serverCfg: authoritative,
|
||||
_cfgRev: 4,
|
||||
_cfgContentFingerprint: fingerprint(authoritative),
|
||||
requestUpdate: () => { updates += 1; },
|
||||
};
|
||||
|
||||
assert.equal(rollbackOptimistic(host, attempt, fingerprint), false);
|
||||
assert.equal(host._serverCfg, authoritative);
|
||||
assert.equal(updates, 0);
|
||||
|
||||
host._cfgRev = 3;
|
||||
host._serverCfg = { settings: { value: 'newer-local-edit' } };
|
||||
assert.equal(rollbackOptimistic(host, attempt, fingerprint), false);
|
||||
assert.equal(host._serverCfg.settings.value, 'newer-local-edit');
|
||||
assert.equal(updates, 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user