fix: lazy delivery survives flaky networks and stale caches (#353)
Network failure of the editor runtime is no longer terminal: the loader
re-arms to idle and the next explicit press starts a fresh cycle, while a
fingerprint mismatch on either attempt stays terminal. The toast now says
what actually helps — retry advice for the network, refresh advice for a
foreign build — via one shared lazyLoadFailureMessage helper (new i18n key
editor.retry_advice in en/ru/de).
The field smoke caught a second, deeper bug on the way: Chromium records a
FAILED module in the page module map permanently, so retrying the same URL
(even the cache-busted one) never touched the network again. Every retry
now carries a per-cycle nonce and becomes a genuinely new module request.
A proxy-cached stale entry no longer kills the card silently: the entry
facade is rewritten at build time from a static re-export into a top-level
`try{await import(...)}catch{...}` — importers keep the happy-path
guarantee (await import(entry) still resolves only after
customElements.define), and the catch defines a fallback element with a
localized "reload the page" panel. Content-hashed chunks are served with
`public, max-age=31536000, immutable`, and verifyBundleTree now fails on
orphan chunks that the manifest does not name.
Proofs: loader units for re-arm/terminality/toast wording + an AST check
that both loaders forward the terminality flag; smoke_entry_stale (en/ru)
against a tree without the main chunk; smoke_lazy_editor_chunk extended —
second press after network failure now really opens the editor; pytest for
the immutable header; orphan-tree unit; five new registry mutants.
TESTING.md budget line updated to the #352 ceiling alongside.
Issue: #353
User-Visible: yes
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b",
|
||||
"fingerprint": "99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 256127,
|
||||
"initialViewGzipBytes": 256828,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"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"
|
||||
],
|
||||
"lazyGzipBytes": 160345,
|
||||
"lazyGzipBytes": 160366,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/editor-Do71pUmD.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 131827,
|
||||
"lazyEditorGzipBytes": 131834,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7197,
|
||||
"lazyOnboardingGzipBytes": 7196,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-AOEY0-Iu.js"
|
||||
"houseplan-assets/de-CFxUrXZ2.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 21873,
|
||||
"lazyLocaleGzipBytes": 21886,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-AOEY0-Iu.js",
|
||||
"sha256": "af2926bc08c1d4d617b844f53b7aa8ef772b9751c15570b78b2fb24ab1109c20",
|
||||
"rawBytes": 71477,
|
||||
"gzipBytes": 21873,
|
||||
"path": "houseplan-assets/de-CFxUrXZ2.js",
|
||||
"sha256": "62ceb0c8d016c48de243b8d643b4f02bae8a623dc8604d115af1420ecb6f7582",
|
||||
"rawBytes": 71541,
|
||||
"gzipBytes": 21886,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-2znZ5Si4.js",
|
||||
"sha256": "f701b93ee31570464f43993fb38d74f142f7e2ff1033e38f6ba79a8c17b6e858",
|
||||
"path": "houseplan-assets/editor-Do71pUmD.js",
|
||||
"sha256": "71dea046e80ff8ce30728044ad3cf85ee536798e230306f5ec1e8824a6e0e1d2",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1582,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"sha256": "6d133886e636a0cd8a38a321f4bec4eac300bd7191631494e895c299b2d9bb6e",
|
||||
"rawBytes": 910936,
|
||||
"gzipBytes": 255944,
|
||||
"path": "houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"sha256": "2040dd1630535449eb8ef3b1a1cfb71da2e14c59b7134b7d64b280dc91b9770f",
|
||||
"rawBytes": 911212,
|
||||
"gzipBytes": 256075,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js"
|
||||
"houseplan-assets/de-CFxUrXZ2.js",
|
||||
"houseplan-assets/editor-Do71pUmD.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"sha256": "a644ece8d0cb490b6c41949e25fa80e64580be2d0d4a0e3fc4e1669b33777f9e",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
|
||||
"sha256": "1d6bd9cf7036174a2d3daba802c1a98c1dc69e118752779be1ad5aa6bd956acd",
|
||||
"rawBytes": 504959,
|
||||
"gzipBytes": 129693,
|
||||
"gzipBytes": 129701,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"sha256": "ad84c9b77ef8b3920a69ba80700c97d558c2f6229d2cb589e9548451ef3328c9",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
|
||||
"sha256": "9777a5eb4166704d294d9972f86d0897ec229e40116eb9def15147cd5bf45007",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6645,
|
||||
"gzipBytes": 6646,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-B9Tp5xY6.js",
|
||||
"sha256": "00bb44aff178ffb82ec71bcd59b67d1a12137ecb4379ec0d606bd613b4447c1b",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 552,
|
||||
"path": "houseplan-assets/space-deletion-3-uORj4C.js",
|
||||
"sha256": "bccd5edc4a4113d78ea05a475b8cadc1175287df2e67327213a95e9647074e25",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 550,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "6c720bd98beee922433c17737fe95dc4c98c69ab182ddcddd05c42a82d50f968",
|
||||
"rawBytes": 190,
|
||||
"gzipBytes": 183,
|
||||
"sha256": "228910d9e3580441d5a1064cb5107e2f2dd80d99b24a325b2b54843eb32f0009",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 753,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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-BsbMwnTm.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__="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
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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-BsbMwnTm.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-B9Tp5xY6.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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__="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">
|
||||
${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 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__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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 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};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BsbMwnTm.js";
|
||||
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)}
|
||||
|
||||
@@ -7,6 +7,10 @@ from pathlib import Path
|
||||
_MANIFEST = "houseplan-assets.json"
|
||||
_ASSET_DIR = "houseplan-assets"
|
||||
|
||||
# Content-hashed chunk bodies never change under their URL (#353 К4). The
|
||||
# stable entry `houseplan-card.js` is served elsewhere and stays uncached.
|
||||
ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable"
|
||||
|
||||
|
||||
def resolve_frontend_asset(frontend_root: Path, filename: object) -> Path | None:
|
||||
"""Resolve one generated JS asset only when the current manifest lists it."""
|
||||
|
||||
@@ -11,7 +11,7 @@ except ImportError: # pragma: no cover - old HA compatibility
|
||||
KEY_HASS = "hass" # type: ignore[assignment]
|
||||
|
||||
from .const import FRONTEND_ASSETS_URL
|
||||
from .frontend_asset_manifest import resolve_frontend_asset
|
||||
from .frontend_asset_manifest import ASSET_CACHE_CONTROL, resolve_frontend_asset
|
||||
|
||||
_FRONTEND_ROOT = Path(__file__).parent / "frontend"
|
||||
|
||||
@@ -30,7 +30,13 @@ class HouseplanFrontendAssetView(HomeAssistantView):
|
||||
)
|
||||
if path is None:
|
||||
raise web.HTTPNotFound()
|
||||
# Chunk names are content-hashed, so a served body can never change
|
||||
# under its URL — immutable caching is safe and keeps a stale proxy
|
||||
# from re-serving an old graph after an update (#353 К4).
|
||||
return web.FileResponse(
|
||||
path,
|
||||
headers={"Cache-Control": "no-cache", "X-Content-Type-Options": "nosniff"},
|
||||
headers={
|
||||
"Cache-Control": ASSET_CACHE_CONTROL,
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// #353 AC3b: a proxy-cached entry `houseplan-card.js` that survived an update
|
||||
// points at a main chunk the manifest-gated server no longer serves. Before
|
||||
// the fix the static re-export aborted the whole module and the card died
|
||||
// silently; now the entry defines a fallback element with a human message,
|
||||
// and `await import(entry)` + `createElement(...).setConfig(...)` stays valid.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { chromium } from 'playwright';
|
||||
import { check, checkAll } from './serve.mjs';
|
||||
|
||||
const entry = readFileSync('dist/houseplan-card.js');
|
||||
const page404 = `<!doctype html><meta charset="utf-8"><body><script type="module">
|
||||
await import('/assets/houseplan-card.js');
|
||||
const card = document.createElement('houseplan-card');
|
||||
card.setConfig({ type: 'custom:houseplan-card' });
|
||||
document.body.appendChild(card);
|
||||
window.__done = true;
|
||||
<\/script></body>`;
|
||||
|
||||
const run = async (locale, expected) => {
|
||||
const browser = await chromium.launch({ args: ['--no-sandbox'] });
|
||||
const page = await (await browser.newContext({ locale })).newPage();
|
||||
let pageErrors = 0;
|
||||
page.on('pageerror', (error) => { pageErrors++; console.log('EXC', error.message); });
|
||||
await page.route('**/*', (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
if (path === '/stale.html') {
|
||||
return route.fulfill({ status: 200, contentType: 'text/html', body: page404 });
|
||||
}
|
||||
if (path === '/assets/houseplan-card.js') {
|
||||
return route.fulfill({ status: 200, contentType: 'text/javascript', body: entry });
|
||||
}
|
||||
// Every hashed chunk of the cached build is gone after the update.
|
||||
return route.fulfill({ status: 404, body: 'nf' });
|
||||
});
|
||||
await page.goto('http://demo.local/stale.html', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => window.__done === true, { timeout: 9000 });
|
||||
const text = await page.locator('houseplan-card').textContent();
|
||||
await browser.close();
|
||||
return { text, pageErrors };
|
||||
};
|
||||
|
||||
const en = await run('en-US');
|
||||
const ru = await run('ru-RU');
|
||||
const out = {
|
||||
entrySurvivesMissingChunk: en.pageErrors === 0 && ru.pageErrors === 0,
|
||||
englishMessageVisible: en.text.includes('House Plan was updated')
|
||||
&& en.text.includes('reload the page'),
|
||||
russianMessageVisible: ru.text.includes('House Plan обновился')
|
||||
&& ru.text.includes('перезагрузите страницу'),
|
||||
};
|
||||
checkAll(out);
|
||||
check('smoke_entry_stale', Object.values(out).every(Boolean));
|
||||
if (Object.values(out).every(Boolean)) console.log('OK');
|
||||
@@ -96,27 +96,36 @@ out.guiEditorLoadsAsynchronously = await gui.page.evaluate(async () => {
|
||||
return editor?.localName === 'houseplan-card-editor';
|
||||
});
|
||||
|
||||
// Two network failures are terminal for this page. View stays live and the
|
||||
// retry is the same immutable chunk with a cache-busting query string.
|
||||
// Two network failures end one load cycle, but are NOT terminal (#353): the
|
||||
// loader re-arms and the next explicit press starts a fresh cycle that heals
|
||||
// once the network is back. The retry inside a cycle is the same immutable
|
||||
// chunk with a cache-busting query string.
|
||||
const failed = await launchColdView();
|
||||
let failedRequests = 0;
|
||||
await failed.page.route(runtimeUrlPattern, async (route) => {
|
||||
failedRequests += 1;
|
||||
await route.abort('failed');
|
||||
});
|
||||
await failed.page.locator('houseplan-card').evaluate((card) => {
|
||||
const pressEditor = () => failed.page.locator('houseplan-card').evaluate((card) => {
|
||||
const root = card.shadowRoot || card.renderRoot;
|
||||
root.querySelectorAll('.modetab')[0]?.click();
|
||||
});
|
||||
await failed.page.waitForFunction(() => window.__card._editorRuntimeLoader.state === 'failed');
|
||||
await pressEditor();
|
||||
await failed.page.waitForFunction(() =>
|
||||
window.__card._editorRuntimeLoader.state === 'idle' && window.__card._toast);
|
||||
out.networkFailureRetriesExactlyOnce = failedRequests === 2;
|
||||
out.networkFailureKeepsView = await failed.page.evaluate(() => {
|
||||
out.networkFailureKeepsViewWithRetryAdvice = await failed.page.evaluate(() => {
|
||||
const card = window.__card;
|
||||
return card._mode === 'view'
|
||||
&& !card._editorRuntime
|
||||
&& card._toast.includes(card._t('editor.load_failed'))
|
||||
&& card._toast.includes(card._t('editor.refresh_advice'));
|
||||
&& card._toast.includes(card._t('editor.retry_advice'));
|
||||
});
|
||||
await failed.page.unroute(runtimeUrlPattern);
|
||||
await pressEditor();
|
||||
await failed.page.waitForFunction(() =>
|
||||
window.__card._editorRuntimeLoader.state === 'ready' && window.__card._mode === 'plan');
|
||||
out.secondPressAfterNetworkFailureOpensEditor = true;
|
||||
|
||||
// A valid module from a different build is no safer than a 404. Both attempts
|
||||
// are fulfilled deliberately so this checks the fingerprint handshake rather
|
||||
@@ -140,7 +149,8 @@ await mismatch.page.locator('houseplan-card').evaluate((card) => {
|
||||
await mismatch.page.waitForFunction(() => window.__card._editorRuntimeLoader.state === 'failed');
|
||||
out.fingerprintMismatchRetriesExactlyOnce = mismatchRequests === 2;
|
||||
out.fingerprintMismatchKeepsView = await mismatch.page.evaluate(() =>
|
||||
window.__card._mode === 'view' && !window.__card._editorRuntime);
|
||||
window.__card._mode === 'view' && !window.__card._editorRuntime
|
||||
&& window.__card._toast.includes(window.__card._t('editor.refresh_advice')));
|
||||
|
||||
await failed.browser.close();
|
||||
await mismatch.browser.close();
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b",
|
||||
"fingerprint": "99af70faf186a79ae24e2901edbf39e13d6388796d8958ab05fc9b1fca671aae",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 256127,
|
||||
"initialViewGzipBytes": 256828,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"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"
|
||||
],
|
||||
"lazyGzipBytes": 160345,
|
||||
"lazyGzipBytes": 160366,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/editor-Do71pUmD.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 131827,
|
||||
"lazyEditorGzipBytes": 131834,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 7197,
|
||||
"lazyOnboardingGzipBytes": 7196,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-AOEY0-Iu.js"
|
||||
"houseplan-assets/de-CFxUrXZ2.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 21873,
|
||||
"lazyLocaleGzipBytes": 21886,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/de-AOEY0-Iu.js",
|
||||
"sha256": "af2926bc08c1d4d617b844f53b7aa8ef772b9751c15570b78b2fb24ab1109c20",
|
||||
"rawBytes": 71477,
|
||||
"gzipBytes": 21873,
|
||||
"path": "houseplan-assets/de-CFxUrXZ2.js",
|
||||
"sha256": "62ceb0c8d016c48de243b8d643b4f02bae8a623dc8604d115af1420ecb6f7582",
|
||||
"rawBytes": 71541,
|
||||
"gzipBytes": 21886,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-2znZ5Si4.js",
|
||||
"sha256": "f701b93ee31570464f43993fb38d74f142f7e2ff1033e38f6ba79a8c17b6e858",
|
||||
"path": "houseplan-assets/editor-Do71pUmD.js",
|
||||
"sha256": "71dea046e80ff8ce30728044ad3cf85ee536798e230306f5ec1e8824a6e0e1d2",
|
||||
"rawBytes": 3831,
|
||||
"gzipBytes": 1582,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"sha256": "6d133886e636a0cd8a38a321f4bec4eac300bd7191631494e895c299b2d9bb6e",
|
||||
"rawBytes": 910936,
|
||||
"gzipBytes": 255944,
|
||||
"path": "houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"sha256": "2040dd1630535449eb8ef3b1a1cfb71da2e14c59b7134b7d64b280dc91b9770f",
|
||||
"rawBytes": 911212,
|
||||
"gzipBytes": 256075,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-AOEY0-Iu.js",
|
||||
"houseplan-assets/editor-2znZ5Si4.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js"
|
||||
"houseplan-assets/de-CFxUrXZ2.js",
|
||||
"houseplan-assets/editor-Do71pUmD.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Dm8qf6N2.js",
|
||||
"sha256": "a644ece8d0cb490b6c41949e25fa80e64580be2d0d4a0e3fc4e1669b33777f9e",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BuT5Y1tf.js",
|
||||
"sha256": "1d6bd9cf7036174a2d3daba802c1a98c1dc69e118752779be1ad5aa6bd956acd",
|
||||
"rawBytes": 504959,
|
||||
"gzipBytes": 129693,
|
||||
"gzipBytes": 129701,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-Dhx_kyQK.js",
|
||||
"sha256": "ad84c9b77ef8b3920a69ba80700c97d558c2f6229d2cb589e9548451ef3328c9",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-D8yGRdVi.js",
|
||||
"sha256": "9777a5eb4166704d294d9972f86d0897ec229e40116eb9def15147cd5bf45007",
|
||||
"rawBytes": 26356,
|
||||
"gzipBytes": 6645,
|
||||
"gzipBytes": 6646,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js",
|
||||
"houseplan-assets/space-deletion-B9Tp5xY6.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js",
|
||||
"houseplan-assets/space-deletion-3-uORj4C.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-deletion-B9Tp5xY6.js",
|
||||
"sha256": "00bb44aff178ffb82ec71bcd59b67d1a12137ecb4379ec0d606bd613b4447c1b",
|
||||
"rawBytes": 911,
|
||||
"gzipBytes": 552,
|
||||
"path": "houseplan-assets/space-deletion-3-uORj4C.js",
|
||||
"sha256": "bccd5edc4a4113d78ea05a475b8cadc1175287df2e67327213a95e9647074e25",
|
||||
"rawBytes": 906,
|
||||
"gzipBytes": 550,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "6c720bd98beee922433c17737fe95dc4c98c69ab182ddcddd05c42a82d50f968",
|
||||
"rawBytes": 190,
|
||||
"gzipBytes": 183,
|
||||
"sha256": "228910d9e3580441d5a1064cb5107e2f2dd80d99b24a325b2b54843eb32f0009",
|
||||
"rawBytes": 1089,
|
||||
"gzipBytes": 753,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BsbMwnTm.js"
|
||||
"houseplan-assets/houseplan-card-g88CD9VI.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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-BsbMwnTm.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__="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
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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-BsbMwnTm.js";import{i as b,c as $,a as v,s as f,t as y}from"./space-deletion-B9Tp5xY6.js";const w=1e3,D=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},S="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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__="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">
|
||||
${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 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__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";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 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};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a43476c101e31e0d3e3020d73f02e6dd01f08436f1f404819f58dfbeeebd8f9b";export{d6 as HouseplanCard}from"./houseplan-assets/houseplan-card-BsbMwnTm.js";
|
||||
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)}
|
||||
|
||||
@@ -23,6 +23,13 @@
|
||||
remains available only while the configuration store is empty
|
||||
([#340](https://github.com/Matysh/houseplan-card/issues/340)).
|
||||
|
||||
- The lazy editor survives flaky networks and updates: after a network
|
||||
failure the "Edit" press retries the download instead of staying dead until
|
||||
a page refresh, the toast says what to do, and a stale cached
|
||||
`houseplan-card.js` after an update shows a "reload the page" panel instead
|
||||
of an empty card. Content-hashed chunks are now served with immutable
|
||||
caching ([#353](https://github.com/Matysh/houseplan-card/issues/353)).
|
||||
|
||||
- A very acute apex between walls of different thickness (say 15 and 30 cm)
|
||||
on legacy plans renders as an honest tip again instead of the "trident":
|
||||
the inner-face convergence accounts for both thicknesses, not just the
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
разрешена только пока хранилище конфигурации пусто
|
||||
([#340](https://github.com/Matysh/houseplan-card/issues/340)).
|
||||
|
||||
- Ленивый редактор переживает нестабильную сеть и обновления: после сетевого
|
||||
сбоя повторное нажатие «Редактировать» пробует загрузку заново, а не молчит
|
||||
до перезагрузки страницы; тост подсказывает, что делать. Устаревший
|
||||
`houseplan-card.js` из кэша после обновления показывает плашку «перезагрузите
|
||||
страницу» вместо пустой карточки. Чанки с контент-хэшами теперь отдаются с
|
||||
иммутабельным кэшированием
|
||||
([#353](https://github.com/Matysh/houseplan-card/issues/353)).
|
||||
|
||||
- Очень острая вершина между стенами разной толщины (например, 15 и 30 см)
|
||||
на старых планах снова рисуется честным остриём, а не «трезубцем»: расчёт
|
||||
смыкания внутренних граней учитывает обе толщины, а не одну наибольшую
|
||||
|
||||
@@ -3336,16 +3336,24 @@ require hands on real hardware — they remain for the human pass.
|
||||
request for `houseplan-editor-runtime-*.js`. The first Plan/Devices/
|
||||
Background intent requests it once; later editor switches do not repeat
|
||||
the request [auto: `smoke_lazy_editor_chunk`].
|
||||
- [ ] Two failed requests, or a runtime with a different build fingerprint,
|
||||
leave mode, camera and plan in View and show the localized refresh advice
|
||||
[auto: `editor-runtime-loader.test`, `smoke_lazy_editor_chunk`].
|
||||
- [ ] Two failed network requests leave mode, camera and plan in View, show
|
||||
the localized retry advice, and the next explicit press starts a fresh
|
||||
load cycle that opens the editor once the network is back. A runtime with
|
||||
a different build fingerprint is terminal and shows the refresh advice
|
||||
(#353) [auto: `editor-runtime-loader.test`, `smoke_lazy_editor_chunk`].
|
||||
- [ ] A cached stale entry whose main chunk the server no longer lists shows a
|
||||
localized "reload the page" panel instead of a silently dead card; hashed
|
||||
chunks are served immutable and an orphan chunk on disk fails the bundle
|
||||
tree check (#353) [auto: `smoke_entry_stale`, `bundle-assets.test`,
|
||||
`test_frontend_assets`].
|
||||
- [ ] An empty installation requests the dedicated onboarding chunk, displays
|
||||
the first-space dialog and still has no editor request. Saving a drawn
|
||||
first space requests the editor once and continues into Plan; async
|
||||
`getConfigElement()` still returns `houseplan-card-editor`
|
||||
[auto: `smoke_lazy_editor_chunk`].
|
||||
- [ ] `bundle:budget` follows transitive static imports and keeps initial View
|
||||
at or below 256000 B gzip. Bundle sync, demo freshness, CI artifacts and
|
||||
at or below 282000 B gzip (#352: ~10% headroom over the calibrated fact,
|
||||
printed with the trend on every run). Bundle sync, demo freshness, CI artifacts and
|
||||
release zip validation fail when any manifest-listed asset is missing or
|
||||
its SHA-256 differs [auto: `bundle-assets.test`, `bundle-freshness.test`,
|
||||
release-contract tests].
|
||||
|
||||
@@ -107,8 +107,12 @@ content-hashed modules. A lone `houseplan-card.js` is not a supported install.
|
||||
|
||||
The ordinary View does not download editor code. The first opening of Plan,
|
||||
Device or Background may therefore take a brief moment. If that internal module
|
||||
cannot be loaded after one retry, the plan stays in View and asks you to refresh
|
||||
the page; no half-open editor is kept.
|
||||
cannot be loaded after one retry, the plan stays in View; no half-open editor is
|
||||
kept. After a network failure the card invites you to check the connection and
|
||||
press again — the next press starts a fresh download. Only when the tab holds
|
||||
code from another build does the advice ask for a page refresh instead. A fully
|
||||
stale proxy-cached `houseplan-card.js` no longer leaves an empty card after an
|
||||
update: it shows a panel asking to reload the page.
|
||||
|
||||
### Permissions
|
||||
|
||||
|
||||
@@ -107,8 +107,13 @@ resources:
|
||||
|
||||
В обычном просмотре код редакторов не загружается. Поэтому первое открытие
|
||||
«Плана», «Устройств» или «Подложки» может занять короткое время. Если внутренний
|
||||
модуль не загрузился и после одной повторной попытки, план остаётся в просмотре
|
||||
и предлагает обновить страницу; наполовину открытого редактора не остаётся.
|
||||
модуль не загрузился и после одной повторной попытки, план остаётся в просмотре;
|
||||
наполовину открытого редактора не остаётся. При сетевом сбое карточка предлагает
|
||||
проверить сеть и нажать ещё раз — повторное нажатие пробует загрузку заново.
|
||||
Если же на вкладке остался код другой версии карточки, поможет только
|
||||
перезагрузка страницы, о чём и говорит подсказка. Полностью устаревший
|
||||
`houseplan-card.js` из кэша прокси после обновления больше не оставляет пустую
|
||||
карточку: вместо неё показывается плашка с просьбой перезагрузить страницу.
|
||||
|
||||
### Права пользователей
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 141 KiB |
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": null,
|
||||
"sourceFingerprint": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"sourceFingerprint": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,8 +15,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "78d328498bd17a9ee1ec44b389d244b61fca9ac52c9aad6631aa85c53233fec0"
|
||||
},
|
||||
"view-touch": {
|
||||
"file": "02-view-touch.png",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "a4aa3af48578ee41594a9b1a6e98b4d39f3324a949a9fd9e63d5854576d5db85"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "3be239a5a494f6776540a9c0049694ee6f9d2ba927c8bcb4b26ee5a69a228a7d"
|
||||
},
|
||||
"space-create": {
|
||||
"file": "03-space-create.png",
|
||||
@@ -37,8 +37,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "a55b463055a565892ca139014e314a8f63fb5301c87fff2e48d7e69210fe9b6c"
|
||||
},
|
||||
"room-contour-close": {
|
||||
"file": "04-room-contour-close.png",
|
||||
@@ -48,8 +48,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "dfad9a27371cfa397a3fd8dce7005f42d24649743f5322105ee894067d3a9f16"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
"file": "05-plan-context-tray.png",
|
||||
@@ -59,8 +59,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "b75f348c632296da5c2fc142d53df613f757e1516acac315be50f5b4557729cc"
|
||||
},
|
||||
"device-editor": {
|
||||
"file": "06-device-editor.png",
|
||||
@@ -70,8 +70,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "8affadfce926fe2f6cf344c33f611c44fcbc79c1969f69c3a6935ff2e99c36c3"
|
||||
},
|
||||
"device-display-preview": {
|
||||
"file": "06-device-display-preview.png",
|
||||
@@ -81,8 +81,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
|
||||
},
|
||||
"background-editor": {
|
||||
"file": "07-background-editor.png",
|
||||
@@ -92,8 +92,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
|
||||
},
|
||||
"room-card": {
|
||||
"file": "08-room-card.png",
|
||||
@@ -103,8 +103,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
|
||||
},
|
||||
"device-info": {
|
||||
"file": "09-device-info.png",
|
||||
@@ -114,8 +114,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "076f7b445baa360e1201aedb8a776f311b10b45de6de32da555039426ae14eb1",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
"sourceSha256": "20360427643d04e7068255e2a64cbdb296bfc772d8c11cfeb0baf753520addb5",
|
||||
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
bundleManifestPlugin,
|
||||
cleanBundleOutputPlugin,
|
||||
editorRuntimeRetryUrlPlugin,
|
||||
entryFallbackPlugin,
|
||||
} from './scripts/bundle-manifest.mjs';
|
||||
|
||||
const SOURCE_FINGERPRINT = sourceFingerprint();
|
||||
@@ -35,6 +36,7 @@ export default {
|
||||
typescript({ compilerOptions: { outDir: 'dist/.ts' } }),
|
||||
terser({ format: { comments: false } }),
|
||||
editorRuntimeRetryUrlPlugin(),
|
||||
entryFallbackPlugin(),
|
||||
bundleManifestPlugin(SOURCE_FINGERPRINT),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -174,6 +174,55 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite the entry facade so a stale cached `houseplan-card.js` fails loudly
|
||||
* instead of killing the card silently (#353 К3). Rollup emits the facade as a
|
||||
* STATIC re-export of the content-hashed main chunk; after an update a proxy-
|
||||
* cached facade points at a chunk the manifest-gated server no longer serves,
|
||||
* and a static import failure aborts the whole module before any code runs.
|
||||
*
|
||||
* The rewrite keeps the happy path intact via top-level await: an importer's
|
||||
* `await import(entry)` does not resolve until the inner import settles, so
|
||||
* `customElements.define` of the real card still happens-before the importer
|
||||
* continues — demo, smokes and golden run unchanged. Only the failure branch
|
||||
* is new: it defines a minimal fallback element with a human message.
|
||||
*
|
||||
* Must run BEFORE bundleManifestPlugin so the manifest hashes the final code.
|
||||
*/
|
||||
export function entryFallbackPlugin() {
|
||||
return {
|
||||
name: 'houseplan-entry-fallback',
|
||||
generateBundle(_options, bundle) {
|
||||
const entry = Object.values(bundle)
|
||||
.find((item) => item.type === 'chunk' && item.isEntry);
|
||||
if (!entry) throw new Error('entry chunk was not emitted');
|
||||
const pattern = /export\{[^}]*\}from(["'])(\.\/houseplan-assets\/[^"']+\.js)\1;?/g;
|
||||
const matches = [...entry.code.matchAll(pattern)];
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(`entry facade re-export count is ${matches.length}, expected 1`);
|
||||
}
|
||||
const asset = matches[0][2];
|
||||
const fallback = 'try{await import("' + asset + '")}'
|
||||
+ '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)}';
|
||||
entry.code = entry.code.replace(pattern, fallback);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Remove only files named by the previous generated manifest. */
|
||||
export function cleanBundleOutputPlugin(outputRoot = 'dist') {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -49,6 +49,17 @@ export function verifyBundleTree(root) {
|
||||
throw new Error(`manifest hash mismatch: ${file.path} (${actual} != ${file.sha256})`);
|
||||
}
|
||||
}
|
||||
// #353 К5: a chunk on disk that the manifest does not name is dead weight —
|
||||
// it would ride into the release zip and mask sync bugs. Fail loudly.
|
||||
const assetDir = resolve(root, 'houseplan-assets');
|
||||
if (existsSync(assetDir)) {
|
||||
const listed = new Set(manifest.files.map((file) => file.path));
|
||||
for (const name of readdirSync(assetDir)) {
|
||||
if (name.endsWith('.js') && !listed.has(`houseplan-assets/${name}`)) {
|
||||
throw new Error(`orphan bundle asset: houseplan-assets/${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
|
||||
@@ -634,6 +634,64 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' // mutant: pointer capture removed\n const plan = resolution.plan;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'lazy-loader-network-failure-terminal',
|
||||
guard: 'node --test --test-name-pattern="#353 AC1" test/editor-runtime-loader.test.mjs',
|
||||
because: 'a transient network failure must re-arm the loader for the next explicit press — '
|
||||
+ 'a terminal state blocks the editor until a page refresh (#353 К1)',
|
||||
patches: [{
|
||||
file: 'src/editor-runtime-loader.ts',
|
||||
find: " this._setState(sawMismatch ? 'failed' : 'idle');",
|
||||
replace: " this._setState('failed');",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'lazy-loader-terminality-hardcoded',
|
||||
guard: 'node --test --test-name-pattern="#353" test/editor-runtime-loader.test.mjs',
|
||||
because: 'a hardcoded terminal flag shows the wrong advice — the network toast must invite '
|
||||
+ 'another press, the mismatch toast must demand a refresh (#353 К2/AC5)',
|
||||
patches: [{
|
||||
file: 'src/editor-runtime-loader.ts',
|
||||
find: ' this.options.failed?.(lastError, { terminal: sawMismatch });',
|
||||
replace: ' this.options.failed?.(lastError, { terminal: true });',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'lazy-chunk-cache-control-reverted',
|
||||
guard: 'node scripts/backend-test-guard.mjs hashed_chunks_are_immutably_cacheable '
|
||||
+ 'tests_backend/test_frontend_assets.py',
|
||||
because: 'content-hashed chunk bodies never change under their URL — reverting to no-cache '
|
||||
+ 'reopens the stale-proxy window and refetches on every editor entry (#353 К4)',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/frontend_asset_manifest.py',
|
||||
find: 'ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable"',
|
||||
replace: 'ASSET_CACHE_CONTROL = "no-cache"',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'bundle-tree-orphans-ignored',
|
||||
guard: 'node --test --test-name-pattern="#353 AC4" test/bundle-assets.test.mjs',
|
||||
because: 'an unlisted chunk on disk rides into the release zip and masks sync bugs — '
|
||||
+ 'the tree check must fail loudly on orphans (#353 К5)',
|
||||
patches: [{
|
||||
file: 'scripts/bundle-tree.mjs',
|
||||
find: " if (name.endsWith('.js') && !listed.has(`houseplan-assets/${name}`)) {\n"
|
||||
+ ' throw new Error(`orphan bundle asset: houseplan-assets/${name}`);\n'
|
||||
+ ' }',
|
||||
replace: " void name; void listed;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'entry-fallback-rewrite-skipped',
|
||||
guard: 'node demo/smoke_entry_stale.mjs',
|
||||
because: 'without the rewrite the entry keeps a STATIC re-export: after an update a cached '
|
||||
+ 'entry aborts before any code runs and the card dies silently (#353 К3)',
|
||||
patches: [{
|
||||
file: 'scripts/bundle-manifest.mjs',
|
||||
find: ' entry.code = entry.code.replace(pattern, fallback);',
|
||||
replace: ' void fallback;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'junction-limit-key-precision-lost',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -54,12 +54,15 @@ export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: [
|
||||
'EditorRuntimeLoader', 'EditorRuntimeLoaderState', 'EditorRuntimeModule',
|
||||
'EditorRuntimeLoaderFailure', 'lazyLoadFailureMessage',
|
||||
'EDITOR_RUNTIME_FINGERPRINT', 'createHouseplanEditorRuntime',
|
||||
],
|
||||
smokes: ['smoke_lazy_editor_chunk.mjs'],
|
||||
because: 'the production-bundle scenario proves that the content-hashed editor chunk stays '
|
||||
+ 'off the View network path, is shared by all three editors, retries once, and refuses an '
|
||||
+ 'incompatible build before editor state is installed (#337)',
|
||||
smokes: ['smoke_lazy_editor_chunk.mjs', 'smoke_entry_stale.mjs'],
|
||||
because: 'the production-bundle scenarios prove that the content-hashed editor chunk stays '
|
||||
+ 'off the View network path, is shared by all three editors, retries once per cycle, '
|
||||
+ 're-arms after a network failure so the next press heals (#353), refuses an incompatible '
|
||||
+ 'build before editor state is installed (#337), and that a stale cached entry shows a '
|
||||
+ 'localized reload panel instead of dying silently (#353)',
|
||||
},
|
||||
{
|
||||
symbols: [
|
||||
|
||||
@@ -5,20 +5,49 @@ export interface EditorRuntimeModule<Runtime> {
|
||||
create(): Runtime;
|
||||
}
|
||||
|
||||
export interface EditorRuntimeLoaderFailure {
|
||||
/** Terminal failures need a page refresh; the rest retry on the next intent. */
|
||||
readonly terminal: boolean;
|
||||
}
|
||||
|
||||
export interface EditorRuntimeLoaderOptions<Runtime> {
|
||||
readonly expectedFingerprint: string;
|
||||
readonly load: (attempt: 0 | 1) => Promise<EditorRuntimeModule<Runtime>>;
|
||||
readonly install: (runtime: Runtime) => void;
|
||||
readonly stateChanged?: (state: EditorRuntimeLoaderState) => void;
|
||||
readonly failed?: (error: unknown) => void;
|
||||
readonly failed?: (error: unknown, info: EditorRuntimeLoaderFailure) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* One toast wording for every lazy-runtime failure (#353 AC5). A terminal
|
||||
* failure means this tab runs code from another build — only a refresh helps.
|
||||
* A network failure heals on the next explicit press, so the advice differs.
|
||||
*/
|
||||
export type LazyLoadFailureKey =
|
||||
| 'editor.load_failed' | 'editor.refresh_advice' | 'editor.retry_advice';
|
||||
|
||||
export function lazyLoadFailureMessage(
|
||||
t: (key: LazyLoadFailureKey) => string,
|
||||
info: EditorRuntimeLoaderFailure,
|
||||
): string {
|
||||
return `${t('editor.load_failed')} ${t(info.terminal ? 'editor.refresh_advice' : 'editor.retry_advice')}`;
|
||||
}
|
||||
|
||||
/** Loader-owned marker: the served module belongs to a different build. */
|
||||
class FingerprintMismatchError extends Error {}
|
||||
|
||||
/**
|
||||
* One atomic lazy-runtime boundary shared by all editor entry points.
|
||||
*
|
||||
* A failed module is retried exactly once. Construction happens before
|
||||
* `install`, so a parse, fingerprint or constructor failure cannot leave a
|
||||
* half-installed editor attached to the View card.
|
||||
* A failed module is retried exactly once per load cycle. Construction happens
|
||||
* before `install`, so a parse, fingerprint or constructor failure cannot
|
||||
* leave a half-installed editor attached to the View card.
|
||||
*
|
||||
* Failure outcomes differ (#353): a fingerprint mismatch on either attempt is
|
||||
* terminal — the tab holds another build and only a refresh helps, so `ensure`
|
||||
* keeps returning `false` without importing again. Any other failure (network,
|
||||
* parse) reports via `failed` and returns the loader to `idle`, so the NEXT
|
||||
* explicit user intent starts a fresh cycle. There are no background retries.
|
||||
*/
|
||||
export class EditorRuntimeLoader<Runtime> {
|
||||
private _state: EditorRuntimeLoaderState = 'idle';
|
||||
@@ -43,11 +72,12 @@ export class EditorRuntimeLoader<Runtime> {
|
||||
|
||||
private async _loadWithRetry(): Promise<boolean> {
|
||||
let lastError: unknown = new Error('Editor runtime did not load');
|
||||
let sawMismatch = false;
|
||||
for (const attempt of [0, 1] as const) {
|
||||
try {
|
||||
const module = await this.options.load(attempt);
|
||||
if (module.fingerprint !== this.options.expectedFingerprint) {
|
||||
throw new Error(
|
||||
throw new FingerprintMismatchError(
|
||||
`Editor runtime fingerprint mismatch: expected ${this.options.expectedFingerprint}, got ${module.fingerprint}`,
|
||||
);
|
||||
}
|
||||
@@ -57,10 +87,11 @@ export class EditorRuntimeLoader<Runtime> {
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
lastError = error;
|
||||
if (error instanceof FingerprintMismatchError) sawMismatch = true;
|
||||
}
|
||||
}
|
||||
this._setState('failed');
|
||||
this.options.failed?.(lastError);
|
||||
this._setState(sawMismatch ? 'failed' : 'idle');
|
||||
this.options.failed?.(lastError, { terminal: sawMismatch });
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,12 @@ import {
|
||||
ModeTransitionController, viewportFromViewBox,
|
||||
type HouseplanMode, type ModeTransitionState, type ModeVisualState, type ModeViewBox,
|
||||
} from './mode-transition';
|
||||
import { EditorRuntimeLoader, type EditorRuntimeLoaderState } from './editor-runtime-loader';
|
||||
import { EditorRuntimeLoader, lazyLoadFailureMessage, type EditorRuntimeLoaderState } from './editor-runtime-loader';
|
||||
|
||||
// Chromium records a FAILED module in the page module map permanently — a
|
||||
// retry of the same URL resolves from that map without touching the network.
|
||||
// Each retry therefore needs a fresh query nonce to become a new module (#353).
|
||||
let hpLazyRetrySeq = 0;
|
||||
import {
|
||||
currentLabs, hashSpace, noteLabsRender, subscribeLabs, type LabsSnapshot,
|
||||
} from './labs';
|
||||
@@ -815,7 +820,7 @@ export class HouseplanCard extends LitElement {
|
||||
? await import('./houseplan-editor-runtime')
|
||||
: await import(/* @vite-ignore */ (() => {
|
||||
const url = new URL(EDITOR_RETRY_ASSET, import.meta.url);
|
||||
url.searchParams.set('hp_retry', CARD_VERSION);
|
||||
url.searchParams.set('hp_retry', `${CARD_VERSION}-${++hpLazyRetrySeq}`);
|
||||
return url.href;
|
||||
})()) as typeof import('./houseplan-editor-runtime');
|
||||
return {
|
||||
@@ -829,9 +834,9 @@ export class HouseplanCard extends LitElement {
|
||||
this._editorRuntime = runtime;
|
||||
},
|
||||
stateChanged: (state) => this._editorRuntimeStateChanged(state),
|
||||
failed: (error) => {
|
||||
failed: (error, info) => {
|
||||
console.error('[houseplan] unable to load editor runtime', error);
|
||||
this._showToast(`${this._t('editor.load_failed')} ${this._t('editor.refresh_advice')}`);
|
||||
this._showToast(lazyLoadFailureMessage((key) => this._t(key), info));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -844,7 +849,7 @@ export class HouseplanCard extends LitElement {
|
||||
? await import('./houseplan-onboarding-runtime')
|
||||
: await import(/* @vite-ignore */ (() => {
|
||||
const url = new URL('__HOUSEPLAN_ONBOARDING_RETRY_ASSET__', import.meta.url);
|
||||
url.searchParams.set('hp_retry', CARD_VERSION);
|
||||
url.searchParams.set('hp_retry', `${CARD_VERSION}-${++hpLazyRetrySeq}`);
|
||||
return url.href;
|
||||
})()) as typeof import('./houseplan-onboarding-runtime');
|
||||
return {
|
||||
@@ -857,9 +862,9 @@ export class HouseplanCard extends LitElement {
|
||||
install: (runtime) => {
|
||||
this._onboardingRuntime = runtime;
|
||||
},
|
||||
failed: (error) => {
|
||||
failed: (error, info) => {
|
||||
console.error('[houseplan] unable to load onboarding runtime', error);
|
||||
this._showToast(`${this._t('editor.load_failed')} ${this._t('editor.refresh_advice')}`);
|
||||
this._showToast(lazyLoadFailureMessage((key) => this._t(key), info));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"editor.loading_aria": "Der Plan-Editor wird geladen",
|
||||
"editor.load_failed": "Der Editor konnte nicht geladen werden.",
|
||||
"editor.refresh_advice": "Seite aktualisieren und erneut versuchen.",
|
||||
"editor.retry_advice": "Verbindung prüfen und erneut drücken.",
|
||||
"color_picker.title": "Farbwähler",
|
||||
"color_picker.hue": "Farbton",
|
||||
"color_picker.saturation": "Sättigung",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"editor.loading_aria": "The plan editor is loading",
|
||||
"editor.load_failed": "Could not load the editor.",
|
||||
"editor.refresh_advice": "Refresh the page and try again.",
|
||||
"editor.retry_advice": "Check your connection and press again.",
|
||||
"color_picker.title": "Color picker",
|
||||
"color_picker.hue": "Hue",
|
||||
"color_picker.saturation": "Saturation",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"editor.loading_aria": "Редактор плана загружается",
|
||||
"editor.load_failed": "Не удалось загрузить редактор.",
|
||||
"editor.refresh_advice": "Обновите страницу и повторите попытку.",
|
||||
"editor.retry_advice": "Проверьте сеть и нажмите ещё раз.",
|
||||
"color_picker.title": "Выбор цвета",
|
||||
"color_picker.hue": "Оттенок",
|
||||
"color_picker.saturation": "Насыщенность",
|
||||
|
||||
@@ -147,3 +147,52 @@ test('bundle tree verification fails for a missing or tampered manifest asset',
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('entry facade fails loudly when the main chunk is unavailable (#353 AC3a)', () => {
|
||||
const entry = readFileSync(new URL('../dist/houseplan-card.js', import.meta.url), 'utf8');
|
||||
assert.match(
|
||||
entry,
|
||||
/^globalThis\.__HOUSEPLAN_BUILD_FINGERPRINT__="[0-9a-f]{64}";/,
|
||||
'the fail-closed fingerprint intro must survive the rewrite',
|
||||
);
|
||||
assert.match(
|
||||
entry,
|
||||
/try\{await import\("\.\/houseplan-assets\/[^"]+\.js"\)\}catch\(/,
|
||||
'the entry must await the main chunk so importers keep the happy-path guarantee',
|
||||
);
|
||||
assert.match(entry, /customElements\.define\("houseplan-card",/);
|
||||
assert.match(entry, /reload the page/);
|
||||
assert.doesNotMatch(
|
||||
entry,
|
||||
/(?:^|;)(?:export|import)[\s{"']/m,
|
||||
'no static import/export may remain — a static edge aborts the module before any code runs',
|
||||
);
|
||||
});
|
||||
|
||||
test('bundle tree verification rejects orphan chunks (#353 AC4)', async () => {
|
||||
const { mkdtempSync, writeFileSync, mkdirSync } = await import('node:fs');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const { verifyBundleTree, sha256Bytes } = await import('../scripts/bundle-tree.mjs');
|
||||
const root = mkdtempSync(join(tmpdir(), 'hp-tree-'));
|
||||
mkdirSync(join(root, 'houseplan-assets'));
|
||||
const entryCode = 'try{await import("./houseplan-assets/main-abc.js")}catch(e){}';
|
||||
const chunkCode = 'export const x = 1;';
|
||||
writeFileSync(join(root, 'houseplan-card.js'), entryCode);
|
||||
writeFileSync(join(root, 'houseplan-assets', 'main-abc.js'), chunkCode);
|
||||
writeFileSync(join(root, 'houseplan-assets.json'), JSON.stringify({
|
||||
schema: 1,
|
||||
fingerprint: 'f'.repeat(64),
|
||||
entry: 'houseplan-card.js',
|
||||
files: [
|
||||
{ path: 'houseplan-card.js', sha256: sha256Bytes(Buffer.from(entryCode)) },
|
||||
{ path: 'houseplan-assets/main-abc.js', sha256: sha256Bytes(Buffer.from(chunkCode)) },
|
||||
],
|
||||
}));
|
||||
assert.equal(verifyBundleTree(root).files.length, 2, 'a clean tree verifies');
|
||||
writeFileSync(join(root, 'houseplan-assets', 'junk-old.js'), 'stale');
|
||||
assert.throws(
|
||||
() => verifyBundleTree(root),
|
||||
/orphan bundle asset: houseplan-assets\/junk-old\.js/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -137,3 +137,106 @@ test('documentation capture materializes the complete bundle tree', () => {
|
||||
assert.match(capture, /import ['"]\.\.\/\.\.\/scripts\/bundle-sync\.mjs['"]/);
|
||||
assert.doesNotMatch(capture, /copyFileSync\(BUNDLE,\s*DEMO_BUNDLE\)/);
|
||||
});
|
||||
|
||||
test('network failure re-arms the loader for the next explicit intent (#353 AC1)', async () => {
|
||||
const attempts = [];
|
||||
const failures = [];
|
||||
let cycles = 0;
|
||||
const loader = new EditorRuntimeLoader({
|
||||
expectedFingerprint: 'same',
|
||||
load: async (attempt) => {
|
||||
if (attempt === 0) cycles++;
|
||||
attempts.push(attempt);
|
||||
if (cycles < 3) throw new Error('net::ERR_INTERNET_DISCONNECTED');
|
||||
return { fingerprint: 'same', create: () => 'runtime' };
|
||||
},
|
||||
install: () => {},
|
||||
failed: (error, info) => failures.push({ error: String(error), terminal: info.terminal }),
|
||||
});
|
||||
|
||||
assert.equal(await loader.ensure(), false);
|
||||
assert.equal(loader.state, 'idle', 'a network failure must not be terminal');
|
||||
assert.deepEqual(attempts, [0, 1]);
|
||||
|
||||
assert.equal(await loader.ensure(), false, 'second press runs a fresh cycle');
|
||||
assert.deepEqual(attempts, [0, 1, 0, 1]);
|
||||
assert.equal(failures.length, 2, 'every failed cycle is reported');
|
||||
assert.ok(failures.every((failure) => failure.terminal === false));
|
||||
|
||||
assert.equal(await loader.ensure(), true, 'third press succeeds');
|
||||
assert.equal(loader.state, 'ready');
|
||||
assert.equal(failures.length, 2);
|
||||
});
|
||||
|
||||
test('fingerprint mismatch on either attempt is terminal (#353 AC2)', async () => {
|
||||
const attempts = [];
|
||||
const failures = [];
|
||||
const loader = new EditorRuntimeLoader({
|
||||
expectedFingerprint: 'entry',
|
||||
load: async (attempt) => {
|
||||
attempts.push(attempt);
|
||||
if (attempt === 0) return { fingerprint: 'other', create: () => 'foreign' };
|
||||
throw new Error('net::ERR_FAILED');
|
||||
},
|
||||
install: () => {},
|
||||
failed: (error, info) => failures.push(info.terminal),
|
||||
});
|
||||
|
||||
assert.equal(await loader.ensure(), false);
|
||||
assert.equal(loader.state, 'failed', 'a foreign build was observed — only a refresh helps');
|
||||
assert.deepEqual(failures, [true]);
|
||||
assert.equal(await loader.ensure(), false);
|
||||
assert.deepEqual(attempts, [0, 1], 'terminal failure never imports again');
|
||||
});
|
||||
|
||||
test('lazy failure toast wording follows terminality (#353 AC5)', async () => {
|
||||
const { lazyLoadFailureMessage } = await import('../test-build/editor-runtime-loader.js');
|
||||
const t = (key) => `<${key}>`;
|
||||
assert.equal(
|
||||
lazyLoadFailureMessage(t, { terminal: true }),
|
||||
'<editor.load_failed> <editor.refresh_advice>',
|
||||
);
|
||||
assert.equal(
|
||||
lazyLoadFailureMessage(t, { terminal: false }),
|
||||
'<editor.load_failed> <editor.retry_advice>',
|
||||
);
|
||||
});
|
||||
|
||||
test('both card loaders route their toast through lazyLoadFailureMessage (#353 AC5)', async () => {
|
||||
const ts = (await import('typescript')).default;
|
||||
const source = readFileSync(join(repoRoot, 'src', 'houseplan-card.ts'), 'utf8');
|
||||
const file = ts.createSourceFile(
|
||||
'houseplan-card.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS,
|
||||
);
|
||||
const callbacks = [];
|
||||
const visit = (node) => {
|
||||
if (ts.isPropertyAssignment(node)
|
||||
&& node.name.getText(file) === 'failed'
|
||||
&& ts.isArrowFunction(node.initializer)) {
|
||||
callbacks.push(node.initializer);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(file);
|
||||
assert.equal(callbacks.length, 2, 'editor and onboarding loaders both declare failed callbacks');
|
||||
for (const callback of callbacks) {
|
||||
assert.equal(callback.parameters.length, 2, 'the failure info parameter must be declared');
|
||||
const infoName = callback.parameters[1].name.getText(file);
|
||||
let forwards = false;
|
||||
const inspect = (node) => {
|
||||
if (ts.isCallExpression(node)
|
||||
&& node.expression.getText(file) === 'lazyLoadFailureMessage'
|
||||
&& node.arguments.length === 2
|
||||
&& node.arguments[1].getText(file) === infoName) {
|
||||
forwards = true;
|
||||
}
|
||||
ts.forEachChild(node, inspect);
|
||||
};
|
||||
inspect(callback.body);
|
||||
assert.ok(
|
||||
forwards,
|
||||
'the failed callback must forward its info argument into lazyLoadFailureMessage — '
|
||||
+ 'a hardcoded terminality would show the wrong advice',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -66,3 +66,11 @@ def test_frontend_asset_refuses_wrong_schema_and_symlink_escape(tmp_path: Path)
|
||||
"schema": 1, "files": [{"path": "houseplan-assets/linked.js"}],
|
||||
}), encoding="utf-8")
|
||||
assert resolve_frontend_asset(root, "linked.js") is None
|
||||
|
||||
|
||||
def test_hashed_chunks_are_immutably_cacheable() -> None:
|
||||
"""#353 AC3-в: content-hashed bodies never change under their URL."""
|
||||
assert _MODULE.ASSET_CACHE_CONTROL == "public, max-age=31536000, immutable"
|
||||
view_source = (_PATH.parent / "frontend_assets.py").read_text(encoding="utf-8")
|
||||
assert "ASSET_CACHE_CONTROL" in view_source
|
||||
assert '"no-cache"' not in view_source
|
||||
|
||||