Compare commits

...
Author SHA1 Message Date
claude[bot] 24b2ff8127 docs: review document for #439
Issue: #439
User-Visible: no
2026-09-03 13:30:01 +00:00
Sergey Matyunin 3883352f43 docs: accept unchanged screenshots for settings rollback
Issue: #439
User-Visible: no
2026-09-03 16:17:39 +03:00
Sergey Matyunin f0574c1fa7 fix: roll back rejected general settings save
Issue: #439
User-Visible: yes
2026-09-03 16:14:56 +03:00
26 changed files with 740 additions and 341 deletions
@@ -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": []
}
File diff suppressed because one or more lines are too long
@@ -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);
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)}
+69
View File
@@ -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,
+55 -55
View File
@@ -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": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="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);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="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)}
+5
View File
@@ -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,
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Если Home Assistant отклоняет сохранение общих настроек, план снова показывает
последнее подтверждённое сервером состояние вместо несохранённого локального
черновика; введённые значения остаются в открытом диалоге для повторной
попытки ([#439](https://github.com/Matysh/houseplan-card/issues/439)).
## v1.71.0-beta.2 — 2026-09-03
- Пользовательские картинки подложки теперь корректно восстанавливаются после
+11 -11
View File
@@ -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"
}
},
+221
View File
@@ -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
```
+12 -11
View File
@@ -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) }));
}
+49
View File
@@ -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;
}
+50 -1
View File
@@ -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);
});