Fix confirmation guards for unrenderable branches

Issue: #417
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-02 16:25:24 +03:00
parent 1fd042d9e0
commit e46220f3eb
23 changed files with 369 additions and 180 deletions
@@ -1,52 +1,52 @@
{
"schema": 1,
"fingerprint": "d90076d3f49d110c3959a6c08a86895a6b98ca810fe122b37b76e22adc38ad73",
"fingerprint": "c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-HIu1PXGU.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290460,
"initialViewGzipBytes": 290566,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/de-Db5jQSzb.js",
"houseplan-assets/editor-DrFXz_Hr.js",
"houseplan-assets/fr-gYskL0hZ.js",
"houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js",
"houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
],
"lazyGzipBytes": 205822,
"lazyGzipBytes": 205825,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/editor-DrFXz_Hr.js",
"houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js"
],
"lazyEditorGzipBytes": 148716,
"lazyEditorGzipBytes": 148718,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
],
"lazyOnboardingGzipBytes": 14032,
"lazyLocaleFiles": [
"houseplan-assets/de-Db5jQSzb.js",
"houseplan-assets/fr-gYskL0hZ.js"
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/fr-ClzwnAMY.js"
],
"lazyLocaleGzipBytes": 50146,
"lazyLocaleGzipBytes": 50147,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CI0weKTj.js",
"sha256": "36f0fbfeccd0ac6981be68fc7446f7b548a5cb40a542b48e07331f2791e7f555",
"path": "houseplan-assets/backdrop-pick-CrHBqjf1.js",
"sha256": "5dfc1f1a537f2026f2b2e0628bde0dde81e62278f9fd07647b748a334af220ac",
"rawBytes": 20636,
"gzipBytes": 7072,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Db5jQSzb.js",
"sha256": "412aaaff8a26a9d7fd6d5e4335711e821ea5ad38cc095b61a3ae56ece4c0a7e9",
"path": "houseplan-assets/de-Bja91KNY.js",
"sha256": "62fc9d40d1de845df4c83a527d1426b927c7b87b2a83da8271e14485edebca51",
"rawBytes": 83981,
"gzipBytes": 25344,
"isEntry": false,
@@ -54,72 +54,72 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DrFXz_Hr.js",
"sha256": "cba3f4d097d84c823946d966677f02fcae841233c3a683e2deda6c7084f1f7f2",
"path": "houseplan-assets/editor-CMVCfgT3.js",
"sha256": "408f5bc440896e6b29eaf3ab59d30960974215e3d0341161675a3e88a8f2700e",
"rawBytes": 3826,
"gzipBytes": 1582,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-gYskL0hZ.js",
"sha256": "6fe8d00dfa1f3b29b669aa81b205547f49778c86441e5723ab3982b970af09b0",
"path": "houseplan-assets/fr-ClzwnAMY.js",
"sha256": "976d7e2294ffccbd4223e2ffba4b5c43697edbb8e1baa37d6b785f6a98c69bcc",
"rawBytes": 86212,
"gzipBytes": 24802,
"gzipBytes": 24803,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-HIu1PXGU.js",
"sha256": "d43a961780a297835fa6b3d8b3ff534897b8376128afa9e7c1db7829dee74ee4",
"rawBytes": 1032568,
"gzipBytes": 289663,
"path": "houseplan-assets/houseplan-card-DoTfrXZx.js",
"sha256": "40cc6422072c6017550b0cbf73a772ae4839e172d1187510f1eb92dd78ccb1d5",
"rawBytes": 1032986,
"gzipBytes": 289769,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Db5jQSzb.js",
"houseplan-assets/editor-DrFXz_Hr.js",
"houseplan-assets/fr-gYskL0hZ.js",
"houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js",
"houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js"
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js",
"sha256": "df085c635b6b02fa11c1b0b5d9f8e87fc56309ed905de32f6eaee5b8e7c10af4",
"path": "houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"sha256": "9d86b76630217ad0b95a8f9f5b3ee8c04776f3450f911b3d24eb5c0d6170e10b",
"rawBytes": 543265,
"gzipBytes": 140062,
"gzipBytes": 140063,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js",
"sha256": "0a7828064aa00462836ba7d7fd7bd975b9a0751894d5546aa5c1bad60b987702",
"path": "houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js",
"sha256": "9e5fb7f22b8a9fba1afd1e5930b8da2e1250a55aac7610d60061d9cb80aa9091",
"rawBytes": 28088,
"gzipBytes": 6960,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "0ac5ff1feac770dfe96547960ca7e5816d4f501fc37cdaf47f5e6cd93d3e427b",
"sha256": "9e0f7a84c95cc7aec1c4e3e20e3a8663e12f418a089e3897dcac67fa1be94ae0",
"rawBytes": 1183,
"gzipBytes": 797,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d90076d3f49d110c3959a6c08a86895a6b98ca810fe122b37b76e22adc38ad73";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-HIu1PXGU.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__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-DoTfrXZx.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d90076d3f49d110c3959a6c08a86895a6b98ca810fe122b37b76e22adc38ad73";try{await import("./houseplan-assets/houseplan-card-HIu1PXGU.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__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";try{await import("./houseplan-assets/houseplan-card-DoTfrXZx.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)}
+123
View File
@@ -15,6 +15,75 @@ const { page, browser } = await launch(
{ width: 390, height: 760 }, 1, [], { hasTouch: true, isMobile: true },
);
// Hold the first German chunk request so the card remains in the real `warm`
// language gate long enough to exercise it. Aborting would immediately select
// the English fallback and could turn this into a test that never enters the
// branch it claims to cover.
const germanAsset = /\/houseplan-assets\/de-[^/?]+\.js(?:\?.*)?$/;
let releaseGerman;
let markGermanStarted;
let markGermanCompleted;
const germanStarted = new Promise((resolve) => { markGermanStarted = resolve; });
const germanCompleted = new Promise((resolve) => { markGermanCompleted = resolve; });
const holdGerman = async (route) => {
markGermanStarted();
await new Promise((resolve) => { releaseGerman = resolve; });
await route.continue();
markGermanCompleted();
};
await page.route(germanAsset, holdGerman);
const warmGate = await page.evaluate(async () => {
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const settle = async () => {
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
};
const originalLanguage = card._config?.language;
card._config = { ...card._config, language: 'de' };
card.requestUpdate();
for (let attempt = 0; attempt < 50 && card._dangerConfirmLocaleGate !== 'warm'; attempt++) {
await new Promise((resolve) => setTimeout(resolve, 10));
await settle();
}
const entered = card._dangerConfirmLocaleGate === 'warm' && card.inert;
const before = root().innerHTML;
const decision = await Promise.race([
card._confirmDanger({
key: 'warm-language-gate',
kind: 'destructive',
title: 'Delete?',
message: 'The plan and all its rooms will be deleted.',
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
}),
new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)),
]);
const result = {
warmLanguageGateActuallyEntered: entered,
warmLanguageGateRefusesImmediately: decision === false,
warmLanguageGateKeepsControllerEmpty: card._dangerConfirm === null
&& card._dangerConfirmController.state === null,
warmLanguageGateKeepsNoChangeDom: root().innerHTML === before
&& !root().querySelector('hp-confirm'),
};
card._config = { ...card._config, language: originalLanguage };
card.requestUpdate();
await settle();
return result;
});
await Promise.race([
germanStarted,
new Promise((_, reject) => setTimeout(() => reject(new Error('German locale request not seen')), 1000)),
]);
releaseGerman();
await germanCompleted;
await page.unroute(germanAsset, holdGerman);
const out = await page.evaluate(async () => {
const result = {};
const card = window.__card;
@@ -152,6 +221,58 @@ const out = await page.evaluate(async () => {
&& (await survivor) === true;
await leaveBranch();
// The exact missed branch from #417: a non-empty model whose active space
// cannot be resolved returns `nothing`, unlike onboarding and fixed-floor
// status cards. An already open request must be cancelled during the same
// update, and a new request must never enter the controller.
const originalSpaceModel = card._spaceModel;
const pendingAtSpaceLoss = card._confirmDanger(request('space-lost-after-open'));
await settle();
card._spaceModel = () => undefined;
card.requestUpdate();
await settle();
const spaceLossDecision = await Promise.race([
pendingAtSpaceLoss,
new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)),
]);
result.lostSpaceBranchIsActuallyEntered = root().childElementCount === 0;
result.openConfirmCancelsWhenSpaceIsLost = spaceLossDecision === false
&& card._dangerConfirm === null && card._dangerConfirmController.state === null;
const refusedWithoutSpace = await Promise.race([
card._confirmDanger(request('space-already-lost')),
new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)),
]);
result.lostSpaceRequestRefusesImmediately = refusedWithoutSpace === false
&& card._dangerConfirm === null && card._dangerConfirmController.state === null
&& dialogs() === 0;
card._spaceModel = originalSpaceModel;
card.requestUpdate();
await settle();
// The two status branches called out by #402 still paint a card, so they
// must keep hosting the confirmation instead of being caught by the new
// lost-space guard.
const originalConfig = card._config;
const originalLoadOk = card._loadOk;
const confirmInFixedBranch = async (loadOk, state) => {
card._loadOk = loadOk;
card._config = { ...originalConfig, floor: 'missing-floor-for-confirm-smoke' };
card.requestUpdate();
await settle();
const branchEntered = !!root().querySelector(`[data-fixed-floor-state="${state}"]`);
const pending = card._confirmDanger(request(`fixed-${state}`));
await settle();
const shown = dialogs() === 1;
await decide(false);
return branchEntered && shown && (await pending) === false;
};
result.fixedFloorPendingStillShowsConfirm = await confirmInFixedBranch(false, 'pending');
result.fixedFloorInvalidStillShowsConfirm = await confirmInFixedBranch(true, 'invalid');
card._loadOk = originalLoadOk;
card._config = originalConfig;
card.requestUpdate();
await settle();
// Тап по «Отмена» — тот же путь, что и клик: на touch подтверждение обязано
// спрашиваться, а не деградировать (TOUCH-SUPPORT § Safety floor).
await enterBranch([]);
@@ -236,6 +357,8 @@ const out = await page.evaluate(async () => {
return result;
});
Object.assign(out, warmGate);
await page.evaluate(async () => {
const element = document.createElement('hp-confirm');
element.token = 4_060_000;
+51 -51
View File
@@ -1,52 +1,52 @@
{
"schema": 1,
"fingerprint": "d90076d3f49d110c3959a6c08a86895a6b98ca810fe122b37b76e22adc38ad73",
"fingerprint": "c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-HIu1PXGU.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290460,
"initialViewGzipBytes": 290566,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/de-Db5jQSzb.js",
"houseplan-assets/editor-DrFXz_Hr.js",
"houseplan-assets/fr-gYskL0hZ.js",
"houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js",
"houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
],
"lazyGzipBytes": 205822,
"lazyGzipBytes": 205825,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/editor-DrFXz_Hr.js",
"houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js"
],
"lazyEditorGzipBytes": 148716,
"lazyEditorGzipBytes": 148718,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
],
"lazyOnboardingGzipBytes": 14032,
"lazyLocaleFiles": [
"houseplan-assets/de-Db5jQSzb.js",
"houseplan-assets/fr-gYskL0hZ.js"
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/fr-ClzwnAMY.js"
],
"lazyLocaleGzipBytes": 50146,
"lazyLocaleGzipBytes": 50147,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CI0weKTj.js",
"sha256": "36f0fbfeccd0ac6981be68fc7446f7b548a5cb40a542b48e07331f2791e7f555",
"path": "houseplan-assets/backdrop-pick-CrHBqjf1.js",
"sha256": "5dfc1f1a537f2026f2b2e0628bde0dde81e62278f9fd07647b748a334af220ac",
"rawBytes": 20636,
"gzipBytes": 7072,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Db5jQSzb.js",
"sha256": "412aaaff8a26a9d7fd6d5e4335711e821ea5ad38cc095b61a3ae56ece4c0a7e9",
"path": "houseplan-assets/de-Bja91KNY.js",
"sha256": "62fc9d40d1de845df4c83a527d1426b927c7b87b2a83da8271e14485edebca51",
"rawBytes": 83981,
"gzipBytes": 25344,
"isEntry": false,
@@ -54,72 +54,72 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DrFXz_Hr.js",
"sha256": "cba3f4d097d84c823946d966677f02fcae841233c3a683e2deda6c7084f1f7f2",
"path": "houseplan-assets/editor-CMVCfgT3.js",
"sha256": "408f5bc440896e6b29eaf3ab59d30960974215e3d0341161675a3e88a8f2700e",
"rawBytes": 3826,
"gzipBytes": 1582,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-gYskL0hZ.js",
"sha256": "6fe8d00dfa1f3b29b669aa81b205547f49778c86441e5723ab3982b970af09b0",
"path": "houseplan-assets/fr-ClzwnAMY.js",
"sha256": "976d7e2294ffccbd4223e2ffba4b5c43697edbb8e1baa37d6b785f6a98c69bcc",
"rawBytes": 86212,
"gzipBytes": 24802,
"gzipBytes": 24803,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-HIu1PXGU.js",
"sha256": "d43a961780a297835fa6b3d8b3ff534897b8376128afa9e7c1db7829dee74ee4",
"rawBytes": 1032568,
"gzipBytes": 289663,
"path": "houseplan-assets/houseplan-card-DoTfrXZx.js",
"sha256": "40cc6422072c6017550b0cbf73a772ae4839e172d1187510f1eb92dd78ccb1d5",
"rawBytes": 1032986,
"gzipBytes": 289769,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Db5jQSzb.js",
"houseplan-assets/editor-DrFXz_Hr.js",
"houseplan-assets/fr-gYskL0hZ.js",
"houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js",
"houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js"
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BHxJWPq7.js",
"sha256": "df085c635b6b02fa11c1b0b5d9f8e87fc56309ed905de32f6eaee5b8e7c10af4",
"path": "houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"sha256": "9d86b76630217ad0b95a8f9f5b3ee8c04776f3450f911b3d24eb5c0d6170e10b",
"rawBytes": 543265,
"gzipBytes": 140062,
"gzipBytes": 140063,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-CXZ19w7L.js",
"sha256": "0a7828064aa00462836ba7d7fd7bd975b9a0751894d5546aa5c1bad60b987702",
"path": "houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js",
"sha256": "9e5fb7f22b8a9fba1afd1e5930b8da2e1250a55aac7610d60061d9cb80aa9091",
"rawBytes": 28088,
"gzipBytes": 6960,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CI0weKTj.js",
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "0ac5ff1feac770dfe96547960ca7e5816d4f501fc37cdaf47f5e6cd93d3e427b",
"sha256": "9e0f7a84c95cc7aec1c4e3e20e3a8663e12f418a089e3897dcac67fa1be94ae0",
"rawBytes": 1183,
"gzipBytes": 797,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-HIu1PXGU.js"
"houseplan-assets/houseplan-card-DoTfrXZx.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d90076d3f49d110c3959a6c08a86895a6b98ca810fe122b37b76e22adc38ad73";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-HIu1PXGU.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__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-DoTfrXZx.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
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__="d90076d3f49d110c3959a6c08a86895a6b98ca810fe122b37b76e22adc38ad73";try{await import("./houseplan-assets/houseplan-card-HIu1PXGU.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__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";try{await import("./houseplan-assets/houseplan-card-DoTfrXZx.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
- Dangerous-action confirmation no longer hangs if the active space disappears
or a request arrives while a lazy language is switching; House Plan now
refuses safely when no confirmation dialog can be rendered
([#417](https://github.com/Matysh/houseplan-card/issues/417)).
## v1.70.0 — 2026-09-02
- House Plan adds a private **Help & feedback** flow, localized backend errors
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Подтверждение опасного действия больше не зависает, если активное пространство
исчезло или запрос пришёл во время переключения лениво загружаемого языка:
когда показать диалог невозможно, House Plan безопасно отвечает отказом
([#417](https://github.com/Matysh/houseplan-card/issues/417)).
## v1.70.0 — 2026-09-02
- В House Plan появился закрытый канал **«Помощь и обратная связь»**, ошибки
+33
View File
@@ -828,6 +828,39 @@ const MUTANT_DEFINITIONS = [
replace: ' return html`${body}`;',
}],
},
{
id: 'danger-confirm-lost-space-request-guard-removed',
guard: 'node demo/smoke_danger_confirm_branches.mjs',
because: 'accepting a new confirmation while the active space resolves to nothing '
+ 'registers a promise whose hp-confirm owner cannot exist (#417 AC1)',
patches: [{
file: 'src/houseplan-card.ts',
find: " if (this._dangerConfirmMissingSpace()) return Promise.resolve(false);",
replace: " void this._dangerConfirmMissingSpace();",
}],
},
{
id: 'danger-confirm-lost-space-transition-cancel-removed',
guard: 'node demo/smoke_danger_confirm_branches.mjs',
because: 'an open confirmation must resolve false before a lost-space render clears '
+ 'its only decision source (#417 AC1)',
patches: [{
file: 'src/houseplan-card.ts',
find: " if (this._dangerConfirm && this._dangerConfirmMissingSpace()) {\n this._cancelDangerConfirm();\n }",
replace: " void this._dangerConfirm;\n void this._dangerConfirmMissingSpace;",
}],
},
{
id: 'danger-confirm-warm-language-guard-removed',
guard: 'node demo/smoke_danger_confirm_branches.mjs',
because: 'the warm language branch returns Lit noChange, so a newly registered '
+ 'confirmation has no rendered decision source and hangs (#417 AC2)',
patches: [{
file: 'src/houseplan-card.ts',
find: " if (this._dangerConfirmLocaleGate === 'warm') return Promise.resolve(false);",
replace: " void this._dangerConfirmLocaleGate;",
}],
},
{
id: 'furniture-edge-handles-steal-the-corner',
guard: 'node demo/smoke_furniture_polish.mjs',
+29 -6
View File
@@ -247,7 +247,9 @@ import {
import { enqueueSerializedWrite } from './serialized-write-queue';
import { hasTranslation, langOf, t, type I18nKey } from './i18n';
import { LANGUAGE_RUNTIME, subscribeLanguageLoadFailures } from './i18n/registry';
import { languageLoadingTemplate, languageRenderGate } from './i18n/language-runtime';
import {
languageLoadingTemplate, languageRenderGate, type LanguageRenderGate,
} from './i18n/language-runtime';
import { CommandStack } from './command-stack';
import {
applyDevicePlacement,
@@ -2148,16 +2150,31 @@ export class HouseplanCard extends LitElement {
} | { kind: 'run'; text: string; exec: () => void } | null = null;
/** One eager confirmation owner shared by View, onboarding and lazy editors. */
private _dangerConfirm: HpConfirmState | null = null;
/** Last language branch painted by `_renderBody`; `warm` cannot accept new dialogs. */
private _dangerConfirmLocaleGate: LanguageRenderGate = 'ready';
private readonly _dangerConfirmController = new HpConfirmController((state) => {
this._dangerConfirm = state;
});
/**
* The only post-initialisation branch whose body is literally `nothing`.
* Onboarding and fixed-floor errors paint a card and can host hp-confirm;
* a lost active space cannot.
*/
private _dangerConfirmMissingSpace(): boolean {
const model = this._model;
if (!model.length) return false;
const fixed = this._fixedFloorState(model);
if (fixed.kind === 'pending' || fixed.kind === 'invalid') return false;
return !this._spaceModel();
}
private _confirmDanger = (request: HpConfirmRequest): Promise<boolean> => {
// #402: a card that draws nothing cannot ask. Refusing outright is the
// honest answer — the alternative is a promise nobody will ever resolve,
// which is exactly the defect this issue closes. The caller already treats
// `false` as «the user declined», and at this point the user has pressed
// nothing: the card is not on screen yet.
// #402/#417: a card that draws nothing cannot ask. Refusing outright is
// the honest answer — the alternative is a promise nobody will ever
// resolve. Keep the guards aligned with `_renderBody`'s two unrenderable
// branches instead of registering a controller request with no DOM owner.
if (!this._config || !this.hass) return Promise.resolve(false);
if (this._dangerConfirmLocaleGate === 'warm') return Promise.resolve(false);
if (this._dangerConfirmMissingSpace()) return Promise.resolve(false);
return this._dangerConfirmController.confirm(request);
};
private _cancelDangerConfirm = (): void => {
@@ -4149,6 +4166,11 @@ export class HouseplanCard extends LitElement {
if (!preserveGeometry) this._cfgEpoch++;
}
this._syncEmptySpaceState();
// #417: losing the only renderable space while a decision is pending must
// settle it before render() clears hp-confirm with `nothing`.
if (this._dangerConfirm && this._dangerConfirmMissingSpace()) {
this._cancelDangerConfirm();
}
if (changed.has('hass') && this.hass) {
this._hassSequence++;
this._renderSnapshotAt = Date.now();
@@ -11284,6 +11306,7 @@ export class HouseplanCard extends LitElement {
const localeGate = languageRenderGate(
this, LANGUAGE_RUNTIME, langOf(this.hass, this._config.language),
);
this._dangerConfirmLocaleGate = localeGate;
if (localeGate === 'cold') return languageLoadingTemplate();
if (localeGate === 'warm') return noChange;
const onboardingRuntimeRequested = !!this._importDialog