fix: a cold View resolves taps without the lazy editor runtime (#357)

Field report from the dacha: the wall switch "Гостиная основной свет",
whose controls name three virtual light sources, periodically ignored taps
— no toggle, no glow — until its settings dialog was opened once with no
changes. "Periodically" was every fresh tab: the #337 lazy split left
_toggleIntent (and the confirm-line helpers) on the card as stubs
delegating into the editor runtime, so a plain View tap on a cold tab
threw `Houseplan editor runtime is not loaded` synchronously inside the
click handler. Opening any editor surface loaded the runtime and "healed"
the tab for its lifetime.

The View card now owns toggle resolution: _toggleIntent calls
resolveToggleIntent directly (device-toggle.ts was already in the initial
graph; the card owns _planHass/_fullRegistryHass/_virtualLights), and
_toggleStateText/_toggleConfirmationStateText/_toggleConfirmationLines
moved with it. The editor runtime delegates back to the host — one source
of truth, editor consumers (dialog preview, hint lines) unchanged.

Every product smoke preloads the runtime, so none of them could see this
class of regression. The new smoke_cold_view_toggle mirrors the field
config on a genuinely cold tab: a real switch drives three passive
virtual lamps with one tap, a controlled lamp drives its switch back,
tap_confirm renders its state lines and confirms, and the editor chunk is
never requested. A registry mutant restores the old delegation and is
killed by that smoke.

Issue: #357
User-Visible: yes
This commit is contained in:
Codex
2026-08-28 21:02:19 +03:00
parent c2cf4a6996
commit 49174e81f7
34 changed files with 650 additions and 396 deletions
@@ -1,97 +1,97 @@
{
"schema": 1,
"fingerprint": "c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43",
"fingerprint": "f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DZo3gC2T.js",
"houseplan-assets/houseplan-card-Bit484fR.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 270793,
"initialViewGzipBytes": 271143,
"lazyFiles": [
"houseplan-assets/de-Cuf6hSGS.js",
"houseplan-assets/editor-Drr9z9rK.js",
"houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/de-Bm8vPaKB.js",
"houseplan-assets/editor-CcQvuoGr.js",
"houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"lazyGzipBytes": 166311,
"lazyGzipBytes": 165964,
"lazyEditorFiles": [
"houseplan-assets/editor-Drr9z9rK.js",
"houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/editor-CcQvuoGr.js",
"houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"lazyEditorGzipBytes": 137204,
"lazyEditorGzipBytes": 136854,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"lazyOnboardingGzipBytes": 7198,
"lazyLocaleFiles": [
"houseplan-assets/de-Cuf6hSGS.js"
"houseplan-assets/de-Bm8vPaKB.js"
],
"lazyLocaleGzipBytes": 22462,
"lazyLocaleGzipBytes": 22465,
"files": [
{
"path": "houseplan-assets/de-Cuf6hSGS.js",
"sha256": "fff8ca0681b5dbd33a6dea7f5d05fbbebdec6caa6659f8b89bfc59fbb1ab50d1",
"path": "houseplan-assets/de-Bm8vPaKB.js",
"sha256": "e8e5acd5cda2ac637e178a08ae4eb4ea1127472b736b8cab7c19ae2ab6b8bb0a",
"rawBytes": 73919,
"gzipBytes": 22462,
"gzipBytes": 22465,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Drr9z9rK.js",
"sha256": "045707acaa6535cf617dedd8492ed8a2285c2cbe995b0fc88f3e263682522a60",
"path": "houseplan-assets/editor-CcQvuoGr.js",
"sha256": "661537fb7547eaaf3d5c6cfd040db88c74ae95b40554998ef0360c8bccfe3de0",
"rawBytes": 3831,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js"
"houseplan-assets/houseplan-card-Bit484fR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DZo3gC2T.js",
"sha256": "1e09aeec8c7ba2de3e665b132e4a4ea8a1f411274b924b4655bd7c9654f94cac",
"rawBytes": 955358,
"gzipBytes": 270040,
"path": "houseplan-assets/houseplan-card-Bit484fR.js",
"sha256": "9069adcc9c0be4411d89d822d3610de6e1154598693cb3cb8575d9eaa6743bcc",
"rawBytes": 956442,
"gzipBytes": 270390,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Cuf6hSGS.js",
"houseplan-assets/editor-Drr9z9rK.js",
"houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js"
"houseplan-assets/de-Bm8vPaKB.js",
"houseplan-assets/editor-CcQvuoGr.js",
"houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"sha256": "9834239d2910161215d70d12147ad83a0fe1d3062abbe6ea9f5165feb8a20d78",
"rawBytes": 521651,
"gzipBytes": 135068,
"path": "houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"sha256": "30bd78227922512c03aa6d0177f776b17508048ff311eac0b8b993e4c3ca0602",
"rawBytes": 520370,
"gzipBytes": 134718,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/houseplan-card-Bit484fR.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js",
"sha256": "cbd7d63ad1fdb1b16b58ba99cbc93a02d4ea6f70317a926018cffe56417c6ba2",
"path": "houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js",
"sha256": "3115311691b5b2164c3fd4d00d311040d1c430a2c0cb6637fafbf1a6775c328a",
"rawBytes": 26356,
"gzipBytes": 6645,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/houseplan-card-Bit484fR.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-CrURpMqK.js",
"sha256": "7c9566acfd9fa8531e97cc955cba29dffae614c3c5e114d6235c71d44b6811b1",
"path": "houseplan-assets/space-deletion-1ap2PeUr.js",
"sha256": "f972b71546589bc2601e2432022d835bf10349f876c32d81fa957767ba72c7c4",
"rawBytes": 911,
"gzipBytes": 553,
"isEntry": false,
@@ -100,12 +100,12 @@
},
{
"path": "houseplan-card.js",
"sha256": "a69e76fc9673ef374d20aceba2d0d160b2a42e9d0b4bcfc3baf616a58c89b93f",
"sha256": "a1123da36e218354a3633fe550fe099f6ffb0fb7062dec34c0b38ba3b912ab6c",
"rawBytes": 1089,
"gzipBytes": 753,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js"
"houseplan-assets/houseplan-card-Bit484fR.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43";import{c as e,l as o,t,d1 as s,A as a,d2 as i,d3 as l,d4 as n,E as r,b as c}from"./houseplan-card-DZo3gC2T.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__="f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467";import{c as e,l as o,t,c$ as s,A as a,d0 as i,d1 as l,d2 as n,E as r,b as c}from"./houseplan-card-Bit484fR.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}
@@ -1 +1 @@
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43";export{t as a,e as c,o as i,r as s,s as t};
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),a=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:a,count:a.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467";export{t as a,e as c,o as i,r as s,s as t};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43";try{await import("./houseplan-assets/houseplan-card-DZo3gC2T.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).":"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__="f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467";try{await import("./houseplan-assets/houseplan-card-Bit484fR.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).":"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)}
+187
View File
@@ -0,0 +1,187 @@
// #357: a plain View tap must work on a COLD tab that never loaded the lazy
// editor runtime (#337). The field failure (dacha, "Гостиная основной свет"):
// a real wall switch whose controls name three passive virtual lamps did
// nothing on tap — _toggleIntent was a stub delegating into the not-yet-loaded
// editor runtime and threw synchronously inside the click handler; opening any
// editor surface "healed" the tab. Every product smoke preloads the runtime
// (launch()), so only a cold-view scenario can see this class of regression.
import { launchColdView, checkAll, finish } from './serve.mjs';
const { page, browser } = await launchColdView();
const requested = [];
page.on('request', (request) => requested.push(new URL(request.url()).pathname));
let pageErrors = 0;
page.on('pageerror', (error) => { pageErrors++; console.log('EXC', error.message); });
const out = await page.evaluate(async () => {
const card = window.__card;
const space = card._model[0];
const room = space.rooms.find((candidate) => candidate.id && candidate.area);
const virtualIds = ['v_cold_1', 'v_cold_2', 'v_cold_3'];
const controllerId = 'd_kettle'; // real switch device — the wall-switch analog
const confirmId = 'v_cold_confirm';
const serverConfig = structuredClone(card._serverCfg);
// The exact field shape: passive virtual lamps owned by a real switch.
serverConfig.markers = [...(serverConfig.markers || []),
...virtualIds.map((id, index) => ({
id,
name: `Cold main light ${index + 1}`,
binding: 'virtual',
is_light: true,
tap_action: 'toggle',
space: space.id,
area: room.area,
room_id: room.id,
})),
{
id: confirmId,
name: 'Cold confirmed lamp',
binding: 'virtual',
is_light: true,
tap_action: 'toggle',
tap_confirm: true,
space: space.id,
area: room.area,
room_id: room.id,
}];
// Demo devices come straight from the registry without explicit markers —
// persist one for the switch exactly like the marker dialog would.
serverConfig.markers.push({
id: controllerId,
binding: `device:${controllerId}`,
name: 'Cold wall switch',
tap_action: 'toggle',
controls: virtualIds.map((id) => `marker:${id}`),
space: space.id,
area: room.area,
room_id: room.id,
});
const serverLayout = { ...card._layout };
[...virtualIds, confirmId].forEach((id, index) => {
serverLayout[id] = { s: space.id, x: 0.2 + index * 0.12, y: 0.72 };
});
const virtual = { rev: 0, config_rev: card._cfgRev, off: [] };
const subscriptions = new Map();
const serviceCalls = [];
let toggleCalls = 0;
const emit = (event, data) => {
for (const listener of subscriptions.get(event) || []) listener({ data });
};
const baseHass = card.hass;
const baseCallWS = baseHass.callWS.bind(baseHass);
const makeHass = () => ({
...baseHass,
states: { ...baseHass.states },
connection: {
...baseHass.connection,
subscribeEvents: async (callback, event) => {
const listeners = subscriptions.get(event) || new Set();
listeners.add(callback);
subscriptions.set(event, listeners);
return () => listeners.delete(callback);
},
},
callService: async (domain, service, data) => {
serviceCalls.push({ domain, service, data });
// The stub HA honestly flips the switch and pushes a fresh state frame.
const target = [].concat(data?.entity_id || [])[0];
const current = hass.states[target]?.state;
const next = service === 'toggle' ? (current === 'on' ? 'off' : 'on')
: service === 'turn_on' ? 'on' : 'off';
const fresh = makeHass();
fresh.states[target] = { ...hass.states[target], state: next };
hass = fresh;
card.hass = fresh;
await card.updateComplete;
},
callWS: async (message) => {
if (message.type === 'houseplan/config/get') return {
config: serverConfig,
rev: virtual.config_rev,
can_write: true,
virtual_lights: structuredClone(virtual),
};
if (message.type === 'houseplan/layout/get') return {
layout: serverLayout,
rev: card._layoutRev,
};
if (message.type === 'houseplan/virtual_light/toggle') {
toggleCalls++;
const off = new Set(virtual.off);
if (off.has(message.marker_id)) off.delete(message.marker_id);
else off.add(message.marker_id);
virtual.off = [...off].sort();
virtual.rev++;
const reply = {
marker_id: message.marker_id,
on: !off.has(message.marker_id),
rev: virtual.rev,
};
queueMicrotask(() => emit('houseplan_virtual_light_updated', reply));
return reply;
}
return baseCallWS(message);
},
});
let hass = makeHass();
hass.states[`switch.kettle`] = { ...hass.states['switch.kettle'], state: 'on' };
card._unsubVirtual?.();
card._unsubVirtual = null;
card.hass = hass;
await card._reloadConfigOnly(true);
card._layout = serverLayout;
card._regSignature = '';
card._maybeRebuildDevices();
card._ensureLiveSyncSubscriptions();
await card.updateComplete;
const waitFor = async (predicate, timeout = 6000) => {
const started = Date.now();
while (!predicate() && Date.now() - started < timeout) {
await new Promise((resolve) => setTimeout(resolve, 40));
}
return !!predicate();
};
await waitFor(() => card._devices.some((device) => device.id === virtualIds[2]));
await card.updateComplete;
const node = (id) => card.renderRoot.querySelector(`.dev[data-id="${id}"]`);
const isOn = (id) => card._stateClass(card._devices.find((d) => d.id === id)).includes('on');
const allVirtualOn = () => virtualIds.every(isOn);
const out = { runtimeColdBefore: !card._editorRuntime };
// Passive lamps follow their controller: with the switch on they glow.
out.initialAllOn = await waitFor(allVirtualOn);
// One tap on the wall switch drives its own entity; all three follow off.
node(controllerId).dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
out.controllerTapCallsSwitch = await waitFor(() => serviceCalls.length === 1
&& [].concat(serviceCalls[0].data?.entity_id || [])[0] === 'switch.kettle');
out.controllerTapTurnsAllOff = await waitFor(() => !virtualIds.some(isOn));
node(controllerId).dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
out.controllerTapTurnsAllOn = await waitFor(() => serviceCalls.length === 2 && allVirtualOn());
// A tap on a controlled lamp drives the same wall switch (wired semantics).
node(virtualIds[0]).dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
out.lampTapDrivesSwitch = await waitFor(() => serviceCalls.length === 3
&& [].concat(serviceCalls[2].data?.entity_id || [])[0] === 'switch.kettle');
out.lampTapTurnsAllOff = await waitFor(() => !virtualIds.some(isOn));
// An independent manual virtual lamp with tap_confirm: the dialog opens with
// rendered state lines and the confirmation toggles it over the WS API.
node(confirmId).dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
await card.updateComplete;
out.confirmDialogShown = !!card._tapConfirm
&& Array.isArray(card._tapConfirm.lines) && card._tapConfirm.lines.length > 0;
card._tapConfirm?.exec?.();
out.confirmExecTogglesVirtual = await waitFor(() => toggleCalls === 1);
card._tapConfirm = null;
out.runtimeColdAfter = !card._editorRuntime;
return out;
});
out.noEditorRuntimeRequest = requested.every((path) => !/houseplan-editor-runtime-/.test(path));
out.noPageErrors = pageErrors === 0;
checkAll(out);
await finish(browser, out);
+45 -45
View File
@@ -1,97 +1,97 @@
{
"schema": 1,
"fingerprint": "c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43",
"fingerprint": "f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DZo3gC2T.js",
"houseplan-assets/houseplan-card-Bit484fR.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 270793,
"initialViewGzipBytes": 271143,
"lazyFiles": [
"houseplan-assets/de-Cuf6hSGS.js",
"houseplan-assets/editor-Drr9z9rK.js",
"houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/de-Bm8vPaKB.js",
"houseplan-assets/editor-CcQvuoGr.js",
"houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"lazyGzipBytes": 166311,
"lazyGzipBytes": 165964,
"lazyEditorFiles": [
"houseplan-assets/editor-Drr9z9rK.js",
"houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/editor-CcQvuoGr.js",
"houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"lazyEditorGzipBytes": 137204,
"lazyEditorGzipBytes": 136854,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"lazyOnboardingGzipBytes": 7198,
"lazyLocaleFiles": [
"houseplan-assets/de-Cuf6hSGS.js"
"houseplan-assets/de-Bm8vPaKB.js"
],
"lazyLocaleGzipBytes": 22462,
"lazyLocaleGzipBytes": 22465,
"files": [
{
"path": "houseplan-assets/de-Cuf6hSGS.js",
"sha256": "fff8ca0681b5dbd33a6dea7f5d05fbbebdec6caa6659f8b89bfc59fbb1ab50d1",
"path": "houseplan-assets/de-Bm8vPaKB.js",
"sha256": "e8e5acd5cda2ac637e178a08ae4eb4ea1127472b736b8cab7c19ae2ab6b8bb0a",
"rawBytes": 73919,
"gzipBytes": 22462,
"gzipBytes": 22465,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Drr9z9rK.js",
"sha256": "045707acaa6535cf617dedd8492ed8a2285c2cbe995b0fc88f3e263682522a60",
"path": "houseplan-assets/editor-CcQvuoGr.js",
"sha256": "661537fb7547eaaf3d5c6cfd040db88c74ae95b40554998ef0360c8bccfe3de0",
"rawBytes": 3831,
"gzipBytes": 1583,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js"
"houseplan-assets/houseplan-card-Bit484fR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DZo3gC2T.js",
"sha256": "1e09aeec8c7ba2de3e665b132e4a4ea8a1f411274b924b4655bd7c9654f94cac",
"rawBytes": 955358,
"gzipBytes": 270040,
"path": "houseplan-assets/houseplan-card-Bit484fR.js",
"sha256": "9069adcc9c0be4411d89d822d3610de6e1154598693cb3cb8575d9eaa6743bcc",
"rawBytes": 956442,
"gzipBytes": 270390,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Cuf6hSGS.js",
"houseplan-assets/editor-Drr9z9rK.js",
"houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js"
"houseplan-assets/de-Bm8vPaKB.js",
"houseplan-assets/editor-CcQvuoGr.js",
"houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CCBmTE0A.js",
"sha256": "9834239d2910161215d70d12147ad83a0fe1d3062abbe6ea9f5165feb8a20d78",
"rawBytes": 521651,
"gzipBytes": 135068,
"path": "houseplan-assets/houseplan-editor-runtime-CWaq9LTE.js",
"sha256": "30bd78227922512c03aa6d0177f776b17508048ff311eac0b8b993e4c3ca0602",
"rawBytes": 520370,
"gzipBytes": 134718,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/houseplan-card-Bit484fR.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-CJAdI7qX.js",
"sha256": "cbd7d63ad1fdb1b16b58ba99cbc93a02d4ea6f70317a926018cffe56417c6ba2",
"path": "houseplan-assets/houseplan-onboarding-runtime-Tm1y6xbk.js",
"sha256": "3115311691b5b2164c3fd4d00d311040d1c430a2c0cb6637fafbf1a6775c328a",
"rawBytes": 26356,
"gzipBytes": 6645,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js",
"houseplan-assets/space-deletion-CrURpMqK.js"
"houseplan-assets/houseplan-card-Bit484fR.js",
"houseplan-assets/space-deletion-1ap2PeUr.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-CrURpMqK.js",
"sha256": "7c9566acfd9fa8531e97cc955cba29dffae614c3c5e114d6235c71d44b6811b1",
"path": "houseplan-assets/space-deletion-1ap2PeUr.js",
"sha256": "f972b71546589bc2601e2432022d835bf10349f876c32d81fa957767ba72c7c4",
"rawBytes": 911,
"gzipBytes": 553,
"isEntry": false,
@@ -100,12 +100,12 @@
},
{
"path": "houseplan-card.js",
"sha256": "a69e76fc9673ef374d20aceba2d0d160b2a42e9d0b4bcfc3baf616a58c89b93f",
"sha256": "a1123da36e218354a3633fe550fe099f6ffb0fb7062dec34c0b38ba3b912ab6c",
"rawBytes": 1089,
"gzipBytes": 753,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DZo3gC2T.js"
"houseplan-assets/houseplan-card-Bit484fR.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43";import{c as e,l as o,t,d1 as s,A as a,d2 as i,d3 as l,d4 as n,E as r,b as c}from"./houseplan-card-DZo3gC2T.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__="f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467";import{c as e,l as o,t,c$ as s,A as a,d0 as i,d1 as l,d2 as n,E as r,b as c}from"./houseplan-card-Bit484fR.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}
@@ -1 +1 @@
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43";export{t as a,e as c,o as i,r as s,s as t};
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),a=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:a,count:a.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467";export{t as a,e as c,o as i,r as s,s as t};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c219ead6633cb4404f86d670c606d3f1c02a69cc43a411cf25ebd6beb45fcd43";try{await import("./houseplan-assets/houseplan-card-DZo3gC2T.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).":"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__="f3b9d9068e440e59a8abb9e4390a828ab8a3ffeb691e2d0777997c1e56c3c467";try{await import("./houseplan-assets/houseplan-card-Bit484fR.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).":"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)}
+7
View File
@@ -2,6 +2,13 @@
## Unreleased
- Tapping a device on a freshly opened page works again without first
visiting any editor screen. A wall switch controlling virtual light sources
(and every other tap-to-toggle device) silently ignored the tap on a cold
tab until the device settings were opened once — the View card now resolves
the tap entirely on its own, without the lazy editor code
([#357](https://github.com/Matysh/houseplan-card/issues/357)).
## v1.69.0-beta.2 — 2026-08-28
- The Background furniture library now has 56 top-view symbols and a compact
+7
View File
@@ -8,6 +8,13 @@
## Не выпущено
- Нажатие на устройство на только что открытой странице снова работает без
предварительного захода в какой-либо редактор. Выключатель, управляющий
виртуальными источниками света (и любое другое устройство с тап-переключением),
молча игнорировал нажатие в холодной вкладке, пока настройки устройства не
откроют один раз — теперь View-карточка разрешает нажатие сама, без ленивого
кода редактора ([#357](https://github.com/Matysh/houseplan-card/issues/357)).
## v1.69.0-beta.2 — 2026-08-28
- В библиотеке мебели редактора подложки теперь 56 видов сверху и компактный
Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 141 KiB

+21 -21
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"sourceFingerprint": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "0ca982495a7749907c48ebbddfd089c54385a4347f79f64e94af2c5a8d5c03eb"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,8 +26,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
},
"space-create": {
"file": "03-space-create.png",
@@ -37,8 +37,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "6b9cd281450e7782cff292f11efc58d227bedd0df86afc3be987feb2edec9dc3"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,8 +114,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "232b156a56885d74f52e69608111212dbfae2a01ee5b1e8f39de695c5b9c70d1",
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
"sourceSha256": "5ccd1afa75c0554258872575cd2681f76563983f762347af5390ffddec00ce41",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
}
}
}
+28
View File
@@ -680,6 +680,34 @@ const MUTANT_DEFINITIONS = [
replace: ' this._languageFailureUnsub = undefined;',
}],
},
{
id: 'cold-view-toggle-delegated-to-runtime',
guard: 'node demo/smoke_cold_view_toggle.mjs',
because: 'the View card must resolve a tap without the lazy editor runtime — the #337 stub '
+ 'threw inside the click handler on every cold tab and a wall switch controlling three '
+ 'virtual lamps did nothing until an editor surface was opened (#357)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' public _toggleIntent(\n'
+ ' device: DevItem,\n'
+ ' devices: readonly DevItem[] = this._devices,\n'
+ ' ): ResolvedToggleIntent | null {\n'
+ ' return resolveToggleIntent({\n'
+ ' hass: this._planHass,\n'
+ ' registryHass: this._fullRegistryHass,\n'
+ ' devices,\n'
+ ' device,\n'
+ ' virtualLights: this._virtualLights,\n'
+ ' });\n'
+ ' }',
replace: ' public _toggleIntent(\n'
+ ' device: DevItem,\n'
+ ' devices: readonly DevItem[] = this._devices,\n'
+ ' ): ResolvedToggleIntent | null {\n'
+ " return this._editorRuntimeOrThrow()._toggleIntent(device, devices);\n"
+ ' }',
}],
},
{
id: 'lazy-loader-network-failure-terminal',
guard: 'node --test --test-name-pattern="#353 AC1" test/editor-runtime-loader.test.mjs',
+11
View File
@@ -44,6 +44,17 @@ export const SMOKE_LINKS = [
+ 'requests and four root render gates: neutral cold frame, atomic German commit, page-cache '
+ 'reuse and the bounded retry-to-English path cannot be proven from pure registry tests (#348)',
},
{
symbols: [
'resolveToggleIntent', 'toggleOperation', 'projectedTapAction',
'toggleOriginOf', 'resolvedLightSources',
],
smokes: ['smoke_cold_view_toggle.mjs'],
because: 'the cold-view scenario proves a plain tap resolves and executes WITHOUT the lazy '
+ 'editor runtime: a wall switch drives three passive virtual lamps, a controlled lamp '
+ 'drives its switch, tap_confirm renders and confirms, and no editor chunk is requested '
+ '(#357 — every warm smoke preloads the runtime and cannot see this class)',
},
{
symbols: ['quantizeKeyCoord', 'INCIDENT_EPS', 'KEY_FACTOR'],
smokes: ['smoke_junction_limits.mjs'],
+61 -8
View File
@@ -12766,11 +12766,25 @@ export class HouseplanCard extends LitElement {
return this._editorRuntimeOrThrow()._markerPreviewDevices(preview);
}
private _toggleIntent(
/**
* #357: toggle resolution belongs to the eager View card. The lazy split
* #337 left these four methods delegating into the editor runtime, so a
* plain tap on a cold tab (kiosk, fresh page) threw synchronously inside
* the click handler until someone opened any editor surface. The resolver
* chain is a pure eager module (device-toggle.ts) over card-owned state —
* the editor runtime now delegates back here, not the other way round.
*/
public _toggleIntent(
device: DevItem,
devices: readonly DevItem[] = this._devices,
): ResolvedToggleIntent | null {
return this._editorRuntimeOrThrow()._toggleIntent(device, devices);
return resolveToggleIntent({
hass: this._planHass,
registryHass: this._fullRegistryHass,
devices,
device,
virtualLights: this._virtualLights,
});
}
private _toggleIntentForDialog(
@@ -12779,18 +12793,57 @@ export class HouseplanCard extends LitElement {
return this._editorRuntimeOrThrow()._toggleIntentForDialog(d);
}
private _toggleStateText(entityId: string, fallback: string): string {
return this._editorRuntimeOrThrow()._toggleStateText(entityId, fallback);
public _toggleStateText(entityId: string, fallback: string): string {
const state = this._planHass?.states?.[entityId] || this.hass?.states?.[entityId];
try {
return state && typeof this.hass?.formatEntityState === 'function'
? this.hass.formatEntityState(state) : fallback;
} catch {
return fallback;
}
}
/** Current-state copy is localized without teaching the dialog command semantics. */
private _toggleConfirmationStateText(target: ResolvedToggleTarget): string {
return this._editorRuntimeOrThrow()._toggleConfirmationStateText(target);
public _toggleConfirmationStateText(target: ResolvedToggleTarget): string {
const raw = String(target.state || 'unknown');
const formatted = target.entityId ? this._toggleStateText(target.entityId, raw) : raw;
if (formatted.trim().toLocaleLowerCase() !== raw.trim().toLocaleLowerCase()) return formatted;
const known: Partial<Record<string, I18nKey>> = {
on: 'confirm.state_on',
off: 'confirm.state_off',
open: 'confirm.state_open',
closed: 'confirm.state_closed',
opening: 'confirm.state_opening',
closing: 'confirm.state_closing',
unknown: 'confirm.state_unknown',
};
const key = known[raw];
if (key) return this._t(key);
// A future integration-specific token stays honest and readable even in a
// harness without HA's formatter; underscores are never exposed as UI.
return raw.replaceAll('_', ' ').replaceAll('-', ' ');
}
/** Snapshot lines shown by Toggle confirmation; execution still re-resolves later. */
private _toggleConfirmationLines(intent: ResolvedToggleIntent): string[] {
return this._editorRuntimeOrThrow()._toggleConfirmationLines(intent);
public _toggleConfirmationLines(intent: ResolvedToggleIntent): string[] {
const effectKeys: Record<Exclude<ToggleNextEffect, 'toggle'>, I18nKey> = {
'turn-on': 'confirm.state_on',
'turn-off': 'confirm.state_off',
open: 'confirm.state_open',
close: 'confirm.state_closed',
stop: 'confirm.state_stopped',
};
return formatToggleConfirmation(intent, {
state: (target) => this._toggleConfirmationStateText(target),
current: (state) => this._t('confirm.current_state', { state }),
expected: (state) => this._t('confirm.expected_state', { state }),
groupCurrent: (on, total) => this._t('confirm.group_current', { on, total }),
groupAllOn: () => this._t('confirm.group_all_on'),
groupAllOff: () => this._t('confirm.group_all_off'),
unavailable: (count) => this._t('confirm.unavailable_targets', { count }),
effect: (effect) => this._t(effectKeys[effect]),
expectedByHa: () => this._t('confirm.expected_by_ha'),
});
}
private _toggleHintLines(intent: ResolvedToggleIntent | null): string[] {
+10 -49
View File
@@ -1082,7 +1082,11 @@ export interface HouseplanEditorHostPort {
_t: (key: I18nKey, vars?: Record<string, string | number>) => string;
_thickWallCuts: () => number[][];
_tip: { x: number; y: number; title: string; meta: string; lqi?: number | null; temp?: number | null; hum?: number | null; } | null;
_toggleConfirmationLines: (intent: ResolvedToggleIntent) => string[];
_toggleConfirmationStateText: (target: ResolvedToggleTarget) => string;
_toggleIntent: (device: DevItem, devices?: readonly DevItem[]) => ResolvedToggleIntent | null;
_toggleMarkerDialogVisibility: () => void;
_toggleStateText: (entityId: string, fallback: string) => string;
_tool: MarkupTool;
_undoKind: "optimize" | "import" | null;
_undoPoint: () => void;
@@ -11630,13 +11634,9 @@ public _toggleIntent(
device: DevItem,
devices: readonly DevItem[] = this.host._devices,
): ResolvedToggleIntent | null {
return resolveToggleIntent({
hass: this.host._planHass,
registryHass: this.host._fullRegistryHass,
devices,
device,
virtualLights: this.host._virtualLights,
});
// #357: the View card owns toggle resolution — a plain tap must work on a
// cold tab that never loaded this runtime. Editor consumers delegate back.
return this.host._toggleIntent(device, devices);
}
public _toggleIntentForDialog(
@@ -11649,54 +11649,15 @@ public _toggleIntentForDialog(
}
public _toggleStateText(entityId: string, fallback: string): string {
const state = this.host._planHass?.states?.[entityId] || this.host.hass?.states?.[entityId];
try {
return state && typeof this.host.hass?.formatEntityState === 'function'
? this.host.hass.formatEntityState(state) : fallback;
} catch {
return fallback;
}
return this.host._toggleStateText(entityId, fallback);
}
public _toggleConfirmationStateText(target: ResolvedToggleTarget): string {
const raw = String(target.state || 'unknown');
const formatted = target.entityId ? this._toggleStateText(target.entityId, raw) : raw;
if (formatted.trim().toLocaleLowerCase() !== raw.trim().toLocaleLowerCase()) return formatted;
const known: Partial<Record<string, I18nKey>> = {
on: 'confirm.state_on',
off: 'confirm.state_off',
open: 'confirm.state_open',
closed: 'confirm.state_closed',
opening: 'confirm.state_opening',
closing: 'confirm.state_closing',
unknown: 'confirm.state_unknown',
};
const key = known[raw];
if (key) return this.host._t(key);
// A future integration-specific token stays honest and readable even in a
// harness without HA's formatter; underscores are never exposed as UI.
return raw.replaceAll('_', ' ').replaceAll('-', ' ');
return this.host._toggleConfirmationStateText(target);
}
public _toggleConfirmationLines(intent: ResolvedToggleIntent): string[] {
const effectKeys: Record<Exclude<ToggleNextEffect, 'toggle'>, I18nKey> = {
'turn-on': 'confirm.state_on',
'turn-off': 'confirm.state_off',
open: 'confirm.state_open',
close: 'confirm.state_closed',
stop: 'confirm.state_stopped',
};
return formatToggleConfirmation(intent, {
state: (target) => this._toggleConfirmationStateText(target),
current: (state) => this.host._t('confirm.current_state', { state }),
expected: (state) => this.host._t('confirm.expected_state', { state }),
groupCurrent: (on, total) => this.host._t('confirm.group_current', { on, total }),
groupAllOn: () => this.host._t('confirm.group_all_on'),
groupAllOff: () => this.host._t('confirm.group_all_off'),
unavailable: (count) => this.host._t('confirm.unavailable_targets', { count }),
effect: (effect) => this.host._t(effectKeys[effect]),
expectedByHa: () => this.host._t('confirm.expected_by_ha'),
});
return this.host._toggleConfirmationLines(intent);
}
public _toggleHintLines(intent: ResolvedToggleIntent | null): string[] {