fix: roll back rejected general settings save

Issue: #439
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-03 13:30:04 +00:00
committed by claude[bot]
parent 5f90eaf4ef
commit d5ad0371aa
24 changed files with 508 additions and 330 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
- Пользовательские картинки подложки теперь корректно восстанавливаются после
+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);
});