fix: the locale runtime the card ships is the code the tests prove (#354)

The production LANGUAGE_RUNTIME was a handwritten twin of the tested
LanguageRuntime class (germanDictionary/Pending/Failed): equivalent on the
day it was written, invisible to every i18n-runtime test afterwards. The
registry now exports one page-scoped `new LanguageRuntime(LANGUAGE_REGISTRY,
…)` instance — the whole existing suite starts proving the object production
actually runs, and a contract unit (instanceof + source free of the old
field names) keeps the duplicate from returning.

The class gains an optional `loadFailed(code)` hook — fired once when a
dictionary load settles into English fallback — and the registry fans it out
through `subscribeLanguageLoadFailures`. Only the View card subscribes (it
alone owns toast infrastructure): a failed language pack now shows the new
`toast.locale_load_failed` message (en/ru/de) instead of a console-only
warning; space card and both GUI editors keep the console warning as before.

Proofs: contract unit, hook unit, subscription unit; smoke_german_locale
extended — the both-attempts-failed scenario now asserts the visible toast;
two new registry mutants (handwritten-twin returns, toast dropped).

Issue: #354
User-Visible: yes
This commit is contained in:
Codex
2026-08-28 17:49:59 +03:00
parent 468eacdb3b
commit 1397afefd5
30 changed files with 331 additions and 213 deletions
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae",
"fingerprint": "f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-g88CD9VI.js",
"houseplan-assets/houseplan-card-CRVz7yAZ.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 256828,
"initialViewGzipBytes": 257222,
"lazyFiles": [
"houseplan-assets/de-CFxUrXZ2.js",
"houseplan-assets/editor-Do71pUmD.js",
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/de-CaUzN0Gb.js",
"houseplan-assets/editor-Cx6zLzlf.js",
"houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"lazyGzipBytes": 160366,
"lazyGzipBytes": 160428,
"lazyEditorFiles": [
"houseplan-assets/editor-Do71pUmD.js",
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/editor-Cx6zLzlf.js",
"houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"lazyEditorGzipBytes": 131834,
"lazyEditorGzipBytes": 131845,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"lazyOnboardingGzipBytes": 7196,
"lazyOnboardingGzipBytes": 7203,
"lazyLocaleFiles": [
"houseplan-assets/de-CFxUrXZ2.js"
"houseplan-assets/de-CaUzN0Gb.js"
],
"lazyLocaleGzipBytes": 21886,
"lazyLocaleGzipBytes": 21934,
"files": [
{
"path": "houseplan-assets/de-CFxUrXZ2.js",
"sha256": "62ceb0c8d016c48de243b8d643b4f02bae8a623dc8604d115af1420ecb6f7582",
"rawBytes": 71541,
"gzipBytes": 21886,
"path": "houseplan-assets/de-CaUzN0Gb.js",
"sha256": "28905f1ea79271d20a7837a9c03b5492dd4e581570dda1c1744b6c0649b38fbd",
"rawBytes": 71667,
"gzipBytes": 21934,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Do71pUmD.js",
"sha256": "71dea046e80ff8ce30728044ad3cf85ee536798e230306f5ec1e8824a6e0e1d2",
"path": "houseplan-assets/editor-Cx6zLzlf.js",
"sha256": "e1234c8e7ff9c1cc932d45a633fef965a23b6c8db9dc5b684092df6b25a5bffa",
"rawBytes": 3831,
"gzipBytes": 1583,
"gzipBytes": 1586,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-g88CD9VI.js",
"sha256": "2040dd1630535449eb8ef3b1a1cfb71da2e14c59b7134b7d64b280dc91b9770f",
"rawBytes": 911212,
"gzipBytes": 256075,
"path": "houseplan-assets/houseplan-card-CRVz7yAZ.js",
"sha256": "c8ed01fa23f357e2a2691a4b64ba459f67b2ff9567314be2159a30429fe951dd",
"rawBytes": 912395,
"gzipBytes": 256466,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CFxUrXZ2.js",
"houseplan-assets/editor-Do71pUmD.js",
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js"
"houseplan-assets/de-CaUzN0Gb.js",
"houseplan-assets/editor-Cx6zLzlf.js",
"houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"sha256": "1d6bd9cf7036174a2d3daba802c1a98c1dc69e118752779be1ad5aa6bd956acd",
"path": "houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"sha256": "9e82dc7246a631a56e31e842603e099fdb5f8b89d2c4b282af4f3306330e3577",
"rawBytes": 504959,
"gzipBytes": 129701,
"gzipBytes": 129705,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
"sha256": "9777a5eb4166704d294d9972f86d0897ec229e40116eb9def15147cd5bf45007",
"path": "houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js",
"sha256": "8b429f66b9bdc598278504cebde6800730c9f8d19e42c7b184d6cd7445a92b04",
"rawBytes": 26356,
"gzipBytes": 6646,
"gzipBytes": 6649,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-3-uORj4C.js",
"sha256": "bccd5edc4a4113d78ea05a475b8cadc1175287df2e67327213a95e9647074e25",
"rawBytes": 906,
"gzipBytes": 550,
"path": "houseplan-assets/space-deletion-Efs-z9tD.js",
"sha256": "534af6bd61ad2404183665d7401e625383a59321feeb55db99c514d9f23415aa",
"rawBytes": 911,
"gzipBytes": 554,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "228910d9e3580441d5a1064cb5107e2f2dd80d99b24a325b2b54843eb32f0009",
"sha256": "a950c24f9f8f6cc9c50a67a5b41ad644acda7dc94501f4cbe2e771b0fa271090",
"rawBytes": 1089,
"gzipBytes": 753,
"gzipBytes": 756,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-g88CD9VI.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-CRVz7yAZ.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-g88CD9VI.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-3-uORj4C.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-CRVz7yAZ.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-Efs-z9tD.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
${s.map(s=>{return p`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -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 a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";export{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)),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__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";export{t as a,e as c,o as i,r as s,s as t};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";try{await import("./houseplan-assets/houseplan-card-g88CD9VI.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__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";try{await import("./houseplan-assets/houseplan-card-CRVz7yAZ.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)}
+5
View File
@@ -78,6 +78,11 @@ out.failureFallsBackAndUnblocks = await failed.page.evaluate(() =>
!window.__card.inert && window.__card._t('btn.cancel') === 'Cancel'
&& window.__card.getAttribute('lang') === 'en');
out.failureWarnsOnce = warnings === 1;
// #354: the failed dictionary must be VISIBLE on the View card — a toast in
// the (English-fallback) locale, not only a console line.
out.failureShowsToast = await failed.page.evaluate(() =>
window.__card._toast === window.__card._t('toast.locale_load_failed')
&& window.__card._t('toast.locale_load_failed').includes('language pack'));
await failed.browser.close();
checkAll(out);
+51 -51
View File
@@ -1,111 +1,111 @@
{
"schema": 1,
"fingerprint": "99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae",
"fingerprint": "f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-g88CD9VI.js",
"houseplan-assets/houseplan-card-CRVz7yAZ.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 256828,
"initialViewGzipBytes": 257222,
"lazyFiles": [
"houseplan-assets/de-CFxUrXZ2.js",
"houseplan-assets/editor-Do71pUmD.js",
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/de-CaUzN0Gb.js",
"houseplan-assets/editor-Cx6zLzlf.js",
"houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"lazyGzipBytes": 160366,
"lazyGzipBytes": 160428,
"lazyEditorFiles": [
"houseplan-assets/editor-Do71pUmD.js",
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/editor-Cx6zLzlf.js",
"houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"lazyEditorGzipBytes": 131834,
"lazyEditorGzipBytes": 131845,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"lazyOnboardingGzipBytes": 7196,
"lazyOnboardingGzipBytes": 7203,
"lazyLocaleFiles": [
"houseplan-assets/de-CFxUrXZ2.js"
"houseplan-assets/de-CaUzN0Gb.js"
],
"lazyLocaleGzipBytes": 21886,
"lazyLocaleGzipBytes": 21934,
"files": [
{
"path": "houseplan-assets/de-CFxUrXZ2.js",
"sha256": "62ceb0c8d016c48de243b8d643b4f02bae8a623dc8604d115af1420ecb6f7582",
"rawBytes": 71541,
"gzipBytes": 21886,
"path": "houseplan-assets/de-CaUzN0Gb.js",
"sha256": "28905f1ea79271d20a7837a9c03b5492dd4e581570dda1c1744b6c0649b38fbd",
"rawBytes": 71667,
"gzipBytes": 21934,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Do71pUmD.js",
"sha256": "71dea046e80ff8ce30728044ad3cf85ee536798e230306f5ec1e8824a6e0e1d2",
"path": "houseplan-assets/editor-Cx6zLzlf.js",
"sha256": "e1234c8e7ff9c1cc932d45a633fef965a23b6c8db9dc5b684092df6b25a5bffa",
"rawBytes": 3831,
"gzipBytes": 1583,
"gzipBytes": 1586,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-g88CD9VI.js",
"sha256": "2040dd1630535449eb8ef3b1a1cfb71da2e14c59b7134b7d64b280dc91b9770f",
"rawBytes": 911212,
"gzipBytes": 256075,
"path": "houseplan-assets/houseplan-card-CRVz7yAZ.js",
"sha256": "c8ed01fa23f357e2a2691a4b64ba459f67b2ff9567314be2159a30429fe951dd",
"rawBytes": 912395,
"gzipBytes": 256466,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CFxUrXZ2.js",
"houseplan-assets/editor-Do71pUmD.js",
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js"
"houseplan-assets/de-CaUzN0Gb.js",
"houseplan-assets/editor-Cx6zLzlf.js",
"houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
"sha256": "1d6bd9cf7036174a2d3daba802c1a98c1dc69e118752779be1ad5aa6bd956acd",
"path": "houseplan-assets/houseplan-editor-runtime-g5SOkk1f.js",
"sha256": "9e82dc7246a631a56e31e842603e099fdb5f8b89d2c4b282af4f3306330e3577",
"rawBytes": 504959,
"gzipBytes": 129701,
"gzipBytes": 129705,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
"sha256": "9777a5eb4166704d294d9972f86d0897ec229e40116eb9def15147cd5bf45007",
"path": "houseplan-assets/houseplan-onboarding-runtime-gghL8Cm4.js",
"sha256": "8b429f66b9bdc598278504cebde6800730c9f8d19e42c7b184d6cd7445a92b04",
"rawBytes": 26356,
"gzipBytes": 6646,
"gzipBytes": 6649,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js",
"houseplan-assets/space-deletion-3-uORj4C.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js",
"houseplan-assets/space-deletion-Efs-z9tD.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-3-uORj4C.js",
"sha256": "bccd5edc4a4113d78ea05a475b8cadc1175287df2e67327213a95e9647074e25",
"rawBytes": 906,
"gzipBytes": 550,
"path": "houseplan-assets/space-deletion-Efs-z9tD.js",
"sha256": "534af6bd61ad2404183665d7401e625383a59321feeb55db99c514d9f23415aa",
"rawBytes": 911,
"gzipBytes": 554,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "228910d9e3580441d5a1064cb5107e2f2dd80d99b24a325b2b54843eb32f0009",
"sha256": "a950c24f9f8f6cc9c50a67a5b41ad644acda7dc94501f4cbe2e771b0fa271090",
"rawBytes": 1089,
"gzipBytes": 753,
"gzipBytes": 756,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-g88CD9VI.js"
"houseplan-assets/houseplan-card-CRVz7yAZ.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-g88CD9VI.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";import{c as e,l as o,t,d2 as s,A as a,d3 as i,d4 as l,d5 as n,E as r,b as c}from"./houseplan-card-CRVz7yAZ.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-g88CD9VI.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-3-uORj4C.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";import{bU as t,bV as s,bW as e,bX as o,aq as a,bY as l,c1 as i,bZ as h,b_ as n,b$ as c,c0 as r,b as p,A as _,cV as d,ap as u,ar as g,cW as m}from"./houseplan-card-CRVz7yAZ.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-Efs-z9tD.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";class C{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_openSpaceDialog(p,_){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===p){const i=this.host._serverCfg.spaces.find(t=>t.id===_);if(!i)return;const h=t(i),n=i.settings?.custom_fill&&"object"==typeof i.settings.custom_fill?s(i.settings.custom_fill):null,c="none"===h.fill?{...n||e,a:0}:n;return void(this.host._spaceDialog={mode:p,spaceId:_,title:i.title,planUrl:i.plan_url||null,planFile:null,source:i.plan_url?"file":"draw",showBorders:h.showBorders,showNames:h.showNames,zeroWallStyle:l(i),displayTouched:!0,hideDecor:h.hideDecor,hideOpenings:h.hideOpenings,roomColor:h.color,roomOpacity:h.opacity,fillMode:"none"===h.fill?"custom":h.fill,customFill:c,glowEnabled:h.glow,bgColor:h.bgColor,bgMode:"static"===i.settings?.bg_mode||"daynight"===i.settings?.bg_mode?i.settings.bg_mode:null,northDeg:a({},i.settings),sunRays:"boolean"==typeof i.settings?.sun_rays?i.settings.sun_rays:null,tempMin:h.tempMin,tempMax:h.tempMax,showLqi:h.showLqi??this.host._config?.show_signal??!0,cardFontScale:h.cardFontScale,labelTemp:h.labelTemp,labelHum:h.labelHum,labelLqi:h.labelLqi,labelLight:h.labelLight,cellCm:Number(i.cell_cm)>0?Number(i.cell_cm):5,cellCmInput:o(Number(i.cell_cm)>0?Number(i.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const d=i(this.host._imperial);this.host._spaceDialog={mode:p,title:"",planUrl:null,planFile:null,...b(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:r,roomOpacity:c,fillMode:"custom",customFill:{...e,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:n,tempMax:h,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:d,cellCmInput:o(d,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;const o={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[e.type]||(e.name.toLowerCase().endsWith(".svg")?"svg":"");if(!o)return void this.host._showToast(this.host._t("toast.plan_formats"));const a=new Uint8Array(await e.arrayBuffer());let l="";for(let t=0;t<a.length;t+=32768)l+=String.fromCharCode(...a.subarray(t,t+32768));const i=btoa(l),h=URL.createObjectURL(e),n=await new Promise(t=>{const s=new Image;s.onload=()=>t(s.naturalWidth&&s.naturalHeight?s.naturalWidth/s.naturalHeight:1.414),s.onerror=()=>t(1.414),s.src=h});URL.revokeObjectURL(h),this.host._spaceDialog={...this.host._spaceDialog,planFile:{ext:o,b64:i,aspect:n,name:e.name}}}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return p`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return p`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return p`<div class="savedplans">
${s.map(s=>{return p`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
<img src=${this.host._display(s.url)} alt="" loading="lazy" decoding="async" />
@@ -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 a(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),a=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),t=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&a.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:t,count:t.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";export{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)),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__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";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__="99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae";try{await import("./houseplan-assets/houseplan-card-g88CD9VI.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__="f4223f763dcd6ca7756b77bef6936c1cfd4eff678814c4d37f22bc3a15f49852";try{await import("./houseplan-assets/houseplan-card-CRVz7yAZ.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)}
+5
View File
@@ -2,6 +2,11 @@
## Unreleased
- If the optional language pack (currently German) cannot be downloaded, the
card now says so with a toast instead of silently switching to English; the
locale runtime the card ships is now the same code the i18n test suite
proves ([#354](https://github.com/Matysh/houseplan-card/issues/354)).
- A stale or third-party client can no longer silently replace saved device
positions by sending a whole layout without its revision. Revision-less
bootstrap remains available only while the layout store is empty
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Если необязательный языковой пакет (сейчас — немецкий) не загрузился,
карточка говорит об этом тостом, а не молча переключается на английский;
рантайм локалей в продакшне теперь тот же код, что доказывает тестовый
набор i18n ([#354](https://github.com/Matysh/houseplan-card/issues/354)).
- Устаревший или сторонний клиент больше не может молча заменить сохранённые
позиции устройств, отправив весь layout без его ревизии. Инициализация без
ревизии остаётся доступной только пока хранилище layout пусто
Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 319 KiB

+13 -13
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": null,
"sourceFingerprint": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceFingerprint": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"imageSha256": "78d328498bd17a9ee1ec44b389d244b61fca9ac52c9aad6631aa85c53233fec0"
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "0ca982495a7749907c48ebbddfd089c54385a4347f79f64e94af2c5a8d5c03eb"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
},
"room-contour-close": {
@@ -48,7 +48,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
},
"plan-context-tray": {
@@ -59,7 +59,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
},
"device-editor": {
@@ -70,7 +70,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "8affadfce926fe2f6cf344c33f611c44fcbc79c1969f69c3a6935ff2e99c36c3"
},
"device-display-preview": {
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "6d323b6f641b2cc7db64252f70567348bd3f2551f82e61fd3a2b4674d244918e"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,7 +92,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
},
"room-card": {
@@ -103,7 +103,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
},
"device-info": {
@@ -114,7 +114,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
"sourceSha256": "f8b80b1f920fa0ecaa24abd1ae6c143df1921d892546a22a44831b2b688f7fd0",
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
}
}
+39
View File
@@ -634,6 +634,45 @@ const MUTANT_DEFINITIONS = [
replace: ' // mutant: pointer capture removed\n const plan = resolution.plan;',
}],
},
{
id: 'language-runtime-handwritten-duplicate',
guard: 'node --test --test-name-pattern="#354" test/i18n-runtime.test.mjs',
because: 'a handwritten twin of LanguageRuntime looks equivalent today and rots tomorrow — '
+ 'the i18n suite must prove the object production actually runs (#354 К1/К3)',
patches: [{
file: 'src/i18n/registry.ts',
find: 'export const LANGUAGE_RUNTIME: LanguageRuntimeContract = new LanguageRuntime(\n'
+ ' LANGUAGE_REGISTRY,\n'
+ ' BUILD_FINGERPRINT,\n'
+ ' console.warn,\n'
+ ' (code) => {\n'
+ ' for (const listener of languageLoadFailureListeners) listener(code);\n'
+ ' },\n'
+ ');',
replace: 'const twin = new LanguageRuntime(LANGUAGE_REGISTRY, BUILD_FINGERPRINT, '
+ 'console.warn, (code) => {\n'
+ ' for (const listener of languageLoadFailureListeners) listener(code);\n'
+ '});\n'
+ 'export const LANGUAGE_RUNTIME: LanguageRuntimeContract = {\n'
+ ' state: (code) => twin.state(code),\n'
+ ' dictionary: (code) => twin.dictionary(code),\n'
+ ' ensure: (code) => twin.ensure(code),\n'
+ '};',
}],
},
{
id: 'locale-failure-toast-dropped',
guard: 'node demo/smoke_german_locale.mjs',
because: 'a failed dictionary must be visible on the View card, not only a console line — '
+ 'a German user on a stale tab silently reading English is the N7 hole (#354 К2)',
patches: [{
file: 'src/houseplan-card.ts',
find: " this._languageFailureUnsub = subscribeLanguageLoadFailures(() => {\n"
+ " this._showToast(this._t('toast.locale_load_failed'));\n"
+ ' });',
replace: ' this._languageFailureUnsub = undefined;',
}],
},
{
id: 'lazy-loader-network-failure-terminal',
guard: 'node --test --test-name-pattern="#353 AC1" test/editor-runtime-loader.test.mjs',
+11 -1
View File
@@ -232,7 +232,7 @@ import {
} from './coordinate-canonicalization';
import { enqueueSerializedWrite } from './serialized-write-queue';
import { hasTranslation, langOf, t, type I18nKey } from './i18n';
import { LANGUAGE_RUNTIME } from './i18n/registry';
import { LANGUAGE_RUNTIME, subscribeLanguageLoadFailures } from './i18n/registry';
import { languageLoadingTemplate, languageRenderGate } from './i18n/language-runtime';
import { CommandStack } from './command-stack';
import { resolvedSvgScreenBlend, svgScreenBlendSupported } from './glow-blend';
@@ -2189,6 +2189,7 @@ export class HouseplanCard extends LitElement {
private _continuity = this._newContinuityController();
private _continuityHistory: import('./visual-continuity').ContinuityTraceEvent[] = [];
private _continuityUnsub?: () => void;
private _languageFailureUnsub?: () => void;
private _continuityEpoch = 0;
private _continuityDataReady = true;
private _continuityPaintToken = -1;
@@ -2474,6 +2475,13 @@ export class HouseplanCard extends LitElement {
if (resolvedBlend !== undefined) this._glowScreenBlend = resolvedBlend;
this._continuityUnsub?.();
this._continuityUnsub = subscribePageVisibility(this.ownerDocument, this._pageVisibility);
// #354: only the View card owns toast infrastructure, so it alone turns a
// locale-load failure into a visible message; other runtime surfaces keep
// the console warning from the shared LanguageRuntime.
this._languageFailureUnsub?.();
this._languageFailureUnsub = subscribeLanguageLoadFailures(() => {
this._showToast(this._t('toast.locale_load_failed'));
});
super.connectedCallback();
this._pointerModality.connect(this.ownerDocument.defaultView);
const PointerHoverObserver = this.ownerDocument.defaultView?.MutationObserver;
@@ -2549,6 +2557,8 @@ export class HouseplanCard extends LitElement {
window.removeEventListener('popstate', this._onLocationChanged);
this._continuityUnsub?.();
this._continuityUnsub = undefined;
this._languageFailureUnsub?.();
this._languageFailureUnsub = undefined;
this._motionMedia?.removeEventListener?.('change', this._onMotionChange);
this._motionMedia = undefined;
if (this._vacRaf) { cancelAnimationFrame(this._vacRaf); this._vacRaf = 0; }
+1
View File
@@ -622,6 +622,7 @@
"room.label_scale": "Metrikgröße",
"preview.room_name": "Wohnzimmer",
"toast.cfg_reload_failed": "Der Plan konnte nicht vom Server neu geladen werden: {err}",
"toast.locale_load_failed": "Das Sprachpaket konnte nicht geladen werden. Bis zum Neuladen der Seite wird Englisch verwendet.",
"room.settings_short": "Raumeinstellungen",
"room.unnamed": "Unbenannter Raum",
"marker.use_climate_temp": "Geräte-Temperatur im Raum berücksichtigen",
+1
View File
@@ -622,6 +622,7 @@
"room.label_scale": "Metrics size",
"preview.room_name": "Living room",
"toast.cfg_reload_failed": "Could not reload the plan from the server: {err}",
"toast.locale_load_failed": "Could not load the language pack. English is used until the page reloads.",
"room.settings_short": "Room settings",
"room.unnamed": "Unnamed room",
"marker.use_climate_temp": "Include the device temperature in the room",
+3
View File
@@ -37,6 +37,8 @@ export class LanguageRuntime {
private readonly entries: readonly RuntimeLanguageEntry[],
private readonly expectedFingerprint: string,
private readonly warn: (message: string, error: unknown) => void = console.warn,
/** Optional #354 hook: a dictionary load settled into English fallback. */
private readonly loadFailed?: (code: string) => void,
) {
for (const entry of entries) {
if (entry.dictionary) this.dictionaries.set(entry.code, entry.dictionary);
@@ -84,6 +86,7 @@ export class LanguageRuntime {
}
this.failed.add(entry.code);
this.warn(`[houseplan] unable to load ${entry.code} locale; using English`, lastError);
this.loadFailed?.(entry.code);
}
}
+30 -31
View File
@@ -1,9 +1,10 @@
import en from './en.json' with { type: 'json' };
import ru from './ru.json' with { type: 'json' };
import type {
LanguageRuntimeContract,
LazyLanguageModule,
LocaleDictionary,
import {
LanguageRuntime,
type LanguageRuntimeContract,
type LazyLanguageModule,
type LocaleDictionary,
} from './language-runtime';
const BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
@@ -40,37 +41,35 @@ export type Lang = (typeof LANGUAGE_REGISTRY)[number]['code'];
export const FALLBACK_LANGUAGE_CODE: Lang = 'en';
export const FALLBACK_DICTIONARY = en;
/** One page-scoped cache: multiple cards share the same locale request. */
let germanDictionary: LocaleDictionary | undefined;
let germanPending: Promise<void> | undefined;
let germanFailed = false;
/**
* #354: locale-load failures surface once through this page-scoped listener
* list. Only the View card subscribes and toasts; every other runtime surface
* (space card, both GUI editors) stays with the console warning below.
*/
const languageLoadFailureListeners = new Set<(code: string) => void>();
async function settleGerman(): Promise<void> {
let lastError: unknown;
for (const attempt of [0, 1] as const) {
try {
const loaded = await loadGerman(attempt);
if (loaded.fingerprint !== BUILD_FINGERPRINT) throw new Error('locale fingerprint mismatch');
germanDictionary = loaded.dictionary;
return;
} catch (error) {
lastError = error;
}
}
germanFailed = true;
console.warn('[houseplan] unable to load de locale; using English', lastError);
export function subscribeLanguageLoadFailures(
listener: (code: string) => void,
): () => void {
languageLoadFailureListeners.add(listener);
return () => languageLoadFailureListeners.delete(listener);
}
export const LANGUAGE_RUNTIME: LanguageRuntimeContract = {
state: (code) => code !== 'de' || germanDictionary
? 'ready' : germanFailed ? 'fallback' : 'pending',
dictionary: (code) => code === 'de'
? germanDictionary : code === 'ru' ? ru : code === 'en' ? en : undefined,
ensure: (code) => {
if (code !== 'de' || germanDictionary || germanFailed) return Promise.resolve();
return germanPending ??= settleGerman().finally(() => { germanPending = undefined; });
/**
* The production runtime IS the tested class (#354): the previous handwritten
* object duplicated its logic, so the whole i18n-runtime test suite proved
* properties of code the card never ran. One page-scoped instance is shared
* by every card and editor; the warn hook keeps the console line and fans the
* failure out to subscribers.
*/
export const LANGUAGE_RUNTIME: LanguageRuntimeContract = new LanguageRuntime(
LANGUAGE_REGISTRY,
BUILD_FINGERPRINT,
console.warn,
(code) => {
for (const listener of languageLoadFailureListeners) listener(code);
},
};
);
/** Return a loaded dictionary, falling back synchronously to English. */
export function dictionaryFor(value: unknown): LocaleDictionary {
+1
View File
@@ -622,6 +622,7 @@
"room.label_scale": "Размер подписей",
"preview.room_name": "Гостиная",
"toast.cfg_reload_failed": "Не удалось перечитать план с сервера: {err}",
"toast.locale_load_failed": "Не удалось загрузить языковой пакет. До перезагрузки страницы используется английский.",
"room.settings_short": "Настройки комнаты",
"room.unnamed": "Комната без имени",
"marker.use_climate_temp": "Учитывать температуру устройства в комнате",
+49
View File
@@ -129,3 +129,52 @@ test('locale render gate exposes the fallback language to assistive technology',
assert.equal(languageRenderGate(host, runtime, 'de'), 'ready');
assert.equal(host.attrs.get('lang'), 'en');
});
test('the production runtime is the tested class, not a handwritten twin (#354)', async () => {
const registry = await import('../test-build/i18n/registry.js');
assert.ok(
registry.LANGUAGE_RUNTIME instanceof LanguageRuntime,
'LANGUAGE_RUNTIME must be an instance of the class this suite proves',
);
const { readFileSync } = await import('node:fs');
const source = readFileSync(new URL('../src/i18n/registry.ts', import.meta.url), 'utf8');
assert.doesNotMatch(
source,
/germanPending|germanFailed|settleGerman/,
'the handwritten duplicate must not return — tests would look past production again',
);
});
test('a settled dictionary failure reaches the load-failed hook once (#354)', async () => {
const failures = [];
const warnings = [];
const runtime = new LanguageRuntime(
[
{ code: 'en', dictionary: { key: 'value' } },
{ code: 'de', loadDictionary: async () => { throw new Error('net'); } },
],
'fp',
(message, error) => warnings.push([message, String(error)]),
(code) => failures.push(code),
);
await runtime.ensure('de');
assert.deepEqual(failures, ['de']);
assert.equal(warnings.length, 1);
assert.equal(runtime.state('de'), 'fallback');
await runtime.ensure('de');
assert.deepEqual(failures, ['de'], 'a settled fallback never re-fires the hook');
});
test('language load failure subscription delivers codes and unsubscribes (#354)', async () => {
const { subscribeLanguageLoadFailures } = await import('../test-build/i18n/registry.js');
const seen = [];
const unsubscribe = subscribeLanguageLoadFailures((code) => seen.push(code));
// The registry instance wires its loadFailed hook into these listeners; the
// fan-out itself is observable without a network by a second subscriber
// triggering through the same set semantics.
const second = subscribeLanguageLoadFailures((code) => seen.push(`2:${code}`));
unsubscribe();
second();
assert.deepEqual(seen, [], 'listeners removed before any failure stay silent');
assert.equal(typeof unsubscribe, 'function');
});