mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
test(adoption): ownership lint, post-write smoke, harness seam and docs (#500)
`test/config-adoption-ownership.test.mjs` pins identity writes to the owner and ratchets body staging (AC1/AC2). `demo/smoke_post_write_adoption.mjs` drives space/delete (both runtimes), Optimize Undo and Import apply with a concurrent backdrop change between the write and the re-read (AC4). Smokes that seed revisions from outside the card keep working through the `seedIdentity` harness seam behind the card's delegate setters. The initial View ceiling is re-centred with the measured fact; ARCHITECTURE.md gets the boundary paragraph. Issue: #500 User-Visible: no
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{m as e,l as o,t,e3 as s,A as a,e4 as i,e5 as l,e6 as n,dU as r,b as c}from"./houseplan-card-CfF5g1xu.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{m as e,l as o,t,e3 as s,A as a,e4 as i,e5 as l,e6 as n,dU as r,b as c}from"./houseplan-card-BKVJQukC.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{dS as r,dT as e,dU as t}from"./houseplan-card-CfF5g1xu.js";const s={},a=r(class extends e{constructor(){super(...arguments),this.ot=s}render(r,e){return e()}update(r,[e,s]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every((r,e)=>r===this.ot[e]))return t}else if(this.ot===e)return t;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,s)}});export{a as i};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{dS as r,dT as t,dU as e}from"./houseplan-card-BKVJQukC.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
|
||||
File diff suppressed because one or more lines are too long
+12
File diff suppressed because one or more lines are too long
+4
@@ -1,9 +1,13 @@
|
||||
<<<<<<<< HEAD:custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-BRKtwSzg.js
|
||||
<<<<<<<< HEAD:custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-BRKtwSzg.js
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5c1b1c61b738b69334f9555c60ee17aa874c642234b0dcca6c358552357c0daf";import{l as t,b2 as s,A as e,x as o,b as a,cl as l,cm as i,cn as h,co as c,a$ as n,cp as r,cu as p,cq as d,cr as _,cs as u,ct as g,cv as m,cw as b,dr as $,ds as v,dt as f,a_ as y,b0 as w,du as D}from"./houseplan-card-yh8-YihB.js";import{c as k,e as S,r as C,a as M}from"./backdrop-pick-BulfpEVa.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="5c1b1c61b738b69334f9555c60ee17aa874c642234b0dcca6c358552357c0daf";class L{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)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
|
||||
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,p="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:r(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:p,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=p(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...m(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await k(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await S(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return C(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_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){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const o=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),a=this.host._spaceDialog,l=a?.saved?.find(s=>s.name===t);if(o&&a&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==a.planUrl)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 a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
|
||||
${s.map(s=>{return a`
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{l as t,b0 as s,A as e,t as a,b as o,cj as l,ck as i,cl as h,cm as c,aZ as n,cn as p,cs as r,co as d,cp as _,cq as u,cr as g,ct as m,cu as b,dp as $,dq as v,dr as f,aY as y,a_ as w,ds as D}from"./houseplan-card-CfF5g1xu.js";import{c as k,e as S,r as C,a as M}from"./backdrop-pick-uUbYkhJU.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";class L{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)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?o`<hp-help data-help-key=${l}
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{l as t,b0 as s,A as e,t as a,b as o,cj as l,ck as i,cl as h,cm as c,aZ as n,cn as p,cs as r,co as d,cp as _,cq as u,cr as g,ct as m,cu as b,dp as $,dq as v,dr as f,aY as y,a_ as w,ds as D}from"./houseplan-card-BKVJQukC.js";import{c as k,e as S,r as C,a as M}from"./backdrop-pick-B1a8Wi9Q.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";class L{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)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?o`<hp-help data-help-key=${l}
|
||||
>>>>>>>> c360bcc9 (test(adoption): ownership lint, post-write smoke, harness seam and docs (#500)):dist/houseplan-assets/houseplan-onboarding-runtime-D0Uo9Pw5.js
|
||||
.text=${a(h,l)} .ariaLabel=${a(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const a=l(e),o=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,r="none"===a.fill?{...o||h,a:0}:o;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:a.showBorders,showNames:a.showNames,zeroWallStyle:p(e),displayTouched:!0,hideDecor:a.hideDecor,hideOpenings:a.hideOpenings,roomColor:a.color,roomOpacity:a.opacity,fillMode:"none"===a.fill?"custom":a.fill,customFill:r,glowEnabled:a.glow,bgColor:a.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:a.tempMin,tempMax:a.tempMax,showLqi:a.showLqi??this.host._config?.show_signal??!0,cardFontScale:a.cardFontScale,labelTemp:a.labelTemp,labelHum:a.labelHum,labelLqi:a.labelLqi,labelLight:a.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=r(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...m(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const a=await k(e);if("reject"===a.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===a.kind)return void(this.host._backdropGuard=a.state);const o=await S(e,a.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:o})}_renderBackdropGuard(){return C(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_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}),a=this.host._spaceDialog;return a&&a.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...a,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const a=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),o=this.host._spaceDialog,l=o?.saved?.find(s=>s.name===t);if(a&&o&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==o.planUrl)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 o`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return o`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return o`<div class="savedplans">
|
||||
${s.map(s=>{return o`
|
||||
>>>>>>>> 1ff91b54 (feat(adoption): one owner for config/layout identity and the adoption sequence):custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-DR7M2bnZ.js
|
||||
|
||||
+10
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +1,12 @@
|
||||
<<<<<<<< HEAD:custom_components/houseplan/frontend/houseplan-assets/summary-panel-editor-DoCYqzD1.js
|
||||
<<<<<<<< HEAD:custom_components/houseplan/frontend/houseplan-assets/summary-panel-editor-DoCYqzD1.js
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5c1b1c61b738b69334f9555c60ee17aa874c642234b0dcca6c358552357c0daf";import{b as a,A as e}from"./houseplan-card-yh8-YihB.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-CPSfTezy.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{b as a,A as e}from"./houseplan-card-CfF5g1xu.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-DZOniG9e.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
>>>>>>>> 1ff91b54 (feat(adoption): one owner for config/layout identity and the adoption sequence):custom_components/houseplan/frontend/houseplan-assets/summary-panel-editor-C77DC5Fh.js
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{b as a,A as e}from"./houseplan-card-BKVJQukC.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-CNC8EpID.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
>>>>>>>> c360bcc9 (test(adoption): ownership lint, post-write smoke, harness seam and docs (#500)):custom_components/houseplan/frontend/houseplan-assets/summary-panel-editor-CtqrpdOR.js
|
||||
<span class="summary-switch-caption"><strong>${b("summary.show_local")}</strong>
|
||||
<small id="summary-local-show-hint">${b("summary.show_local_hint")}</small></span>
|
||||
<input id="summary-local-show" data-summary-local-show type="checkbox"
|
||||
|
||||
+8
File diff suppressed because one or more lines are too long
@@ -196,7 +196,8 @@ const out = await page.evaluate(async () => {
|
||||
card._saveConfigDebounced.cancel();
|
||||
card._persistLayout.cancel();
|
||||
card._writeChain = Promise.resolve();
|
||||
card._adoptStructuralResponses = () => ({ configChanged: false, layoutChanged: false });
|
||||
// #500: the delete path re-reads and adopts through the one gated entry; keep it inert here.
|
||||
card._adoptAuthoritative = async () => ({ status: 'adopted', spaceChanged: false });
|
||||
for (const [name, runtime] of [['editor', editor], ['onboarding', onboarding]]) {
|
||||
card._space = 'another-space';
|
||||
card._serverCfg = { spaces: [baseSpace()], markers: [], settings: {} };
|
||||
|
||||
@@ -37,7 +37,7 @@ const res = await page.evaluate(async () => {
|
||||
if (message.type === 'houseplan/config/get') {
|
||||
// #397 AC3: the reconnect path reads BOTH answers, and a config that
|
||||
// differs clears the history for its own reason (`configChanged` in
|
||||
// `_adoptStructuralResponses`). To isolate the layout question the
|
||||
// `adoptStructuralResponses`, #500). To isolate the layout question the
|
||||
// fake server echoes the config the card already holds.
|
||||
return { config: structuredClone(c._serverCfg), rev: c._cfgRev };
|
||||
}
|
||||
@@ -294,7 +294,7 @@ const res = await page.evaluate(async () => {
|
||||
placement: structuredClone(rawProbe) },
|
||||
});
|
||||
// #397 AC3: the reconnect path (`_loadFromServer` →
|
||||
// `_adoptStructuralResponses`) is the OTHER reader of the same value: it
|
||||
// `adoptAuthoritativeGated`, #500) is the OTHER reader of the same value: it
|
||||
// compares the stored `_layoutContentFingerprint` with the server answer
|
||||
// instead of recomputing it. It must be exercised right after a write,
|
||||
// before any reload has had a chance to align the two sides — otherwise
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// #500 AC4: the four post-write adoptions — space/delete from both runtimes,
|
||||
// Optimize Undo and Import apply — pass the same backdrop readiness gate as a
|
||||
// reload before the structure is replaced, and take config/layout revisions
|
||||
// from the re-read `config/get`/`layout/get`, never from the write reply.
|
||||
// A second client changes `plan_url` between our write and the re-read; the
|
||||
// card must prepare THAT backdrop first and end on the re-read revision.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch();
|
||||
const out = await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
await card._ensureOnboardingRuntime();
|
||||
const editor = card._editorRuntime;
|
||||
const onboarding = card._onboardingRuntime;
|
||||
const adoption = card._adoption;
|
||||
const result = {};
|
||||
|
||||
const space = (id, title, planUrl) => ({
|
||||
id, title, plan_url: planUrl, view_box: [0, 0, 1, 1], rooms: [], wall_segments: [], partitions: [],
|
||||
});
|
||||
const freshConfig = () => ({
|
||||
model_version: 9,
|
||||
spaces: [space('alpha', 'Alpha', '/local/alpha.png'), space('beta', 'Beta', '/local/beta.png')],
|
||||
markers: [],
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const order = [];
|
||||
const originalPrepareImage = card._signer.prepareImage.bind(card._signer);
|
||||
card._signer.prepareImage = async (_hass, href) => { order.push(`prepare:${href}`); return true; };
|
||||
const originalAdoptResponses = adoption.adoptResponses.bind(adoption);
|
||||
adoption.adoptResponses = (...args) => { order.push('adopt'); return originalAdoptResponses(...args); };
|
||||
card._confirmDanger = async () => true;
|
||||
card._showToast = () => {};
|
||||
|
||||
// Fake server: our write bumps both revisions; a concurrent client then
|
||||
// swaps the backdrop and bumps the config revision again before the re-read.
|
||||
let server;
|
||||
let concurrentHref = '';
|
||||
const concurrentWrite = () => {
|
||||
for (const s of server.config.spaces) s.plan_url = concurrentHref;
|
||||
server.cfgRev += 1;
|
||||
};
|
||||
const originalHass = card.hass;
|
||||
card.hass = {
|
||||
...originalHass,
|
||||
callWS: async (message) => {
|
||||
switch (message.type) {
|
||||
case 'houseplan/space/delete': {
|
||||
server.config.spaces = server.config.spaces.filter((s) => s.id !== message.space_id);
|
||||
server.cfgRev += 1; server.layRev += 1;
|
||||
const reply = { config_rev: server.cfgRev, layout_rev: server.layRev };
|
||||
concurrentWrite();
|
||||
return reply;
|
||||
}
|
||||
case 'houseplan/plan/optimize_undo': {
|
||||
server.cfgRev += 1; server.layRev += 1;
|
||||
const reply = { config_rev: server.cfgRev, layout_rev: server.layRev };
|
||||
concurrentWrite();
|
||||
return reply;
|
||||
}
|
||||
case 'houseplan/import/apply': {
|
||||
server.cfgRev += 1; server.layRev += 1;
|
||||
const reply = { kind: 'full', config_rev: server.cfgRev, layout_rev: server.layRev, counts: { spaces: 2, rooms: 0, markers: 0 } };
|
||||
concurrentWrite();
|
||||
return reply;
|
||||
}
|
||||
case 'houseplan/config/get':
|
||||
return { config: structuredClone(server.config), rev: server.cfgRev, can_write: true };
|
||||
case 'houseplan/layout/get':
|
||||
return { layout: structuredClone(server.layout), rev: server.layRev };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const reset = async (scenario) => {
|
||||
server = { config: freshConfig(), layout: {}, cfgRev: 10, layRev: 20 };
|
||||
concurrentHref = `media-source://image/${scenario}-500`;
|
||||
adoption.restoreCached({ config: structuredClone(server.config), rev: 10, layout: {}, layout_rev: 20 });
|
||||
card._space = 'beta';
|
||||
card._spaceDialog = null;
|
||||
card._backupImportDialog = null;
|
||||
order.length = 0;
|
||||
await card.updateComplete;
|
||||
};
|
||||
const verdict = (scenario, extra = {}) => {
|
||||
const prepared = order.indexOf(`prepare:${concurrentHref}`);
|
||||
const adopted = order.indexOf('adopt');
|
||||
result[`${scenario}PreparesConcurrentBackdropBeforeAdoption`] = prepared >= 0 && adopted > prepared;
|
||||
result[`${scenario}RevisionsFromReRead`] = card._cfgRev === server.cfgRev && card._layoutRev === server.layRev;
|
||||
result[`${scenario}AdoptsConcurrentBackdrop`] = card._serverCfg.spaces.every((s) => s.plan_url === concurrentHref);
|
||||
Object.assign(result, extra);
|
||||
};
|
||||
|
||||
const spaceDialog = (spaceId) => ({
|
||||
mode: 'edit', spaceId, title: 'Beta', planUrl: null, planFile: null, source: 'draw',
|
||||
showBorders: true, showNames: true, zeroWallStyle: 'dashed', displayTouched: true,
|
||||
hideDecor: false, hideOpenings: false, roomColor: '#888888', roomOpacity: 1, bgColor: null,
|
||||
bgMode: null, northDeg: null, sunRays: null, fillMode: 'custom', customFill: null,
|
||||
glowEnabled: true, tempMin: 15, tempMax: 30, showLqi: true, cardFontScale: 1,
|
||||
labelTemp: true, labelHum: true, labelLqi: true, labelLight: true, cellCm: 5,
|
||||
busy: false, saved: [],
|
||||
});
|
||||
|
||||
for (const [name, runtime] of [['onboardingDelete', onboarding], ['editorDelete', editor]]) {
|
||||
await reset(name);
|
||||
card._spaceDialog = spaceDialog('beta');
|
||||
await runtime._deleteSpace();
|
||||
// the write reply carried cfgRev 11; the concurrent client moved it to 12
|
||||
verdict(name, {
|
||||
[`${name}RevisionNotFromDeleteReply`]: card._cfgRev === 12 && server.cfgRev === 12,
|
||||
[`${name}LeavesDeletedSpace`]: card._space === 'alpha' && card._serverCfg.spaces.length === 1,
|
||||
[`${name}ClosesDialog`]: card._spaceDialog === null,
|
||||
});
|
||||
}
|
||||
|
||||
await reset('optimizeUndo');
|
||||
card._canOptimizeUndo = true;
|
||||
card._undoKind = 'optimize';
|
||||
await editor._undoPlanOptimization();
|
||||
verdict('optimizeUndo', { optimizeUndoClearsUndo: card._canOptimizeUndo === false && card._undoKind === null });
|
||||
|
||||
await reset('importApply');
|
||||
card._backupImportDialog = {
|
||||
filename: 'plan.json', size: 1, token: 'token-500', preview: { confirmation_required: false, counts: {} },
|
||||
expectedConfigRev: card._cfgRev, expectedLayoutRev: card._layoutRev,
|
||||
duplicatePolicy: 'skip', confirmMissing: false, busy: false, error: '',
|
||||
};
|
||||
const fixedFloor = !!card._hasFixedFloor;
|
||||
await editor._applyBackupImport();
|
||||
verdict('importApply', {
|
||||
importApplyClosesDialog: card._backupImportDialog === null,
|
||||
// today's rule, not its absence: a fixed-floor card re-selects through
|
||||
// _adoptInitialSpace, otherwise the previous space is kept
|
||||
importApplyKeepsSpaceRule: fixedFloor ? typeof card._space === 'string' && card._space.length > 0 : card._space === 'beta',
|
||||
});
|
||||
|
||||
card.hass = originalHass;
|
||||
card._signer.prepareImage = originalPrepareImage;
|
||||
delete adoption.adoptResponses;
|
||||
await card.updateComplete;
|
||||
return result;
|
||||
});
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
@@ -471,16 +471,18 @@ const recovery = await page.evaluate(async () => {
|
||||
const originalHass = card.hass;
|
||||
const originalCallWS = originalHass.callWS;
|
||||
const originalPrepareImage = card._signer.prepareImage.bind(card._signer);
|
||||
const originalAdoptStructuralResponses = card._adoptStructuralResponses.bind(card);
|
||||
// #500: adoption lives in the identity owner; observe it there.
|
||||
const adoption = card._adoption;
|
||||
const originalAdoptResponses = adoption.adoptResponses.bind(adoption);
|
||||
const recoveryOrder = [];
|
||||
const concurrentBackdrop = 'media-source://image/summary-recovery-490';
|
||||
card._signer.prepareImage = async (_hass, href) => {
|
||||
recoveryOrder.push(`prepare:${href}`);
|
||||
return true;
|
||||
};
|
||||
card._adoptStructuralResponses = (...args) => {
|
||||
adoption.adoptResponses = (...args) => {
|
||||
recoveryOrder.push('adopt');
|
||||
return originalAdoptStructuralResponses(...args);
|
||||
return originalAdoptResponses(...args);
|
||||
};
|
||||
const writes = [];
|
||||
let serverConfig = structuredClone(card._serverCfg);
|
||||
@@ -540,7 +542,7 @@ const recovery = await page.evaluate(async () => {
|
||||
runtime.dialog = null;
|
||||
card.hass = { ...card.hass, callWS: originalCallWS };
|
||||
card._signer.prepareImage = originalPrepareImage;
|
||||
card._adoptStructuralResponses = originalAdoptStructuralResponses;
|
||||
delete adoption.adoptResponses;
|
||||
await card.updateComplete;
|
||||
return {
|
||||
lostAckClosesAsSuccess: firstClosed,
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{m as e,l as o,t,e3 as s,A as a,e4 as i,e5 as l,e6 as n,dU as r,b as c}from"./houseplan-card-CfF5g1xu.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{m as e,l as o,t,e3 as s,A as a,e4 as i,e5 as l,e6 as n,dU as r,b as c}from"./houseplan-card-BKVJQukC.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
-1
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{dS as r,dT as e,dU as t}from"./houseplan-card-CfF5g1xu.js";const s={},a=r(class extends e{constructor(){super(...arguments),this.ot=s}render(r,e){return e()}update(r,[e,s]){if(Array.isArray(e)){if(Array.isArray(this.ot)&&this.ot.length===e.length&&e.every((r,e)=>r===this.ot[e]))return t}else if(this.ot===e)return t;return this.ot=Array.isArray(e)?Array.from(e):e,this.render(e,s)}});export{a as i};
|
||||
+1
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{dS as r,dT as t,dU as e}from"./houseplan-card-BKVJQukC.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +1,13 @@
|
||||
<<<<<<<< HEAD:dist/houseplan-assets/houseplan-onboarding-runtime-BRKtwSzg.js
|
||||
<<<<<<<< HEAD:dist/houseplan-assets/houseplan-onboarding-runtime-BRKtwSzg.js
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5c1b1c61b738b69334f9555c60ee17aa874c642234b0dcca6c358552357c0daf";import{l as t,b2 as s,A as e,x as o,b as a,cl as l,cm as i,cn as h,co as c,a$ as n,cp as r,cu as p,cq as d,cr as _,cs as u,ct as g,cv as m,cw as b,dr as $,ds as v,dt as f,a_ as y,b0 as w,du as D}from"./houseplan-card-yh8-YihB.js";import{c as k,e as S,r as C,a as M}from"./backdrop-pick-BulfpEVa.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="5c1b1c61b738b69334f9555c60ee17aa874c642234b0dcca6c358552357c0daf";class L{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)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
|
||||
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,p="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:r(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:p,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=p(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...m(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await k(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await S(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return C(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_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){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const o=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),a=this.host._spaceDialog,l=a?.saved?.find(s=>s.name===t);if(o&&a&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==a.planUrl)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 a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
|
||||
${s.map(s=>{return a`
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{l as t,b0 as s,A as e,t as a,b as o,cj as l,ck as i,cl as h,cm as c,aZ as n,cn as p,cs as r,co as d,cp as _,cq as u,cr as g,ct as m,cu as b,dp as $,dq as v,dr as f,aY as y,a_ as w,ds as D}from"./houseplan-card-CfF5g1xu.js";import{c as k,e as S,r as C,a as M}from"./backdrop-pick-uUbYkhJU.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";class L{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)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?o`<hp-help data-help-key=${l}
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{l as t,b0 as s,A as e,t as a,b as o,cj as l,ck as i,cl as h,cm as c,aZ as n,cn as p,cs as r,co as d,cp as _,cq as u,cr as g,ct as m,cu as b,dp as $,dq as v,dr as f,aY as y,a_ as w,ds as D}from"./houseplan-card-BKVJQukC.js";import{c as k,e as S,r as C,a as M}from"./backdrop-pick-B1a8Wi9Q.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";class L{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)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?o`<hp-help data-help-key=${l}
|
||||
>>>>>>>> c360bcc9 (test(adoption): ownership lint, post-write smoke, harness seam and docs (#500)):custom_components/houseplan/frontend/houseplan-assets/houseplan-onboarding-runtime-D0Uo9Pw5.js
|
||||
.text=${a(h,l)} .ariaLabel=${a(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const a=l(e),o=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,r="none"===a.fill?{...o||h,a:0}:o;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:a.showBorders,showNames:a.showNames,zeroWallStyle:p(e),displayTouched:!0,hideDecor:a.hideDecor,hideOpenings:a.hideOpenings,roomColor:a.color,roomOpacity:a.opacity,fillMode:"none"===a.fill?"custom":a.fill,customFill:r,glowEnabled:a.glow,bgColor:a.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:a.tempMin,tempMax:a.tempMax,showLqi:a.showLqi??this.host._config?.show_signal??!0,cardFontScale:a.cardFontScale,labelTemp:a.labelTemp,labelHum:a.labelHum,labelLqi:a.labelLqi,labelLight:a.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=r(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...m(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:_,tempMax:d,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const a=await k(e);if("reject"===a.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===a.kind)return void(this.host._backdropGuard=a.state);const o=await S(e,a.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:o})}_renderBackdropGuard(){return C(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_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}),a=this.host._spaceDialog;return a&&a.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...a,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const a=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),o=this.host._spaceDialog,l=o?.saved?.find(s=>s.name===t);if(a&&o&&l&&l.url===e.url&&l.modified===e.modified&&!l.used_by.length&&l.url!==o.planUrl)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 o`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return o`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return o`<div class="savedplans">
|
||||
${s.map(s=>{return o`
|
||||
>>>>>>>> 1ff91b54 (feat(adoption): one owner for config/layout identity and the adoption sequence):dist/houseplan-assets/houseplan-onboarding-runtime-DR7M2bnZ.js
|
||||
|
||||
File diff suppressed because one or more lines are too long
+23
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +1,12 @@
|
||||
<<<<<<<< HEAD:dist/houseplan-assets/summary-panel-editor-DoCYqzD1.js
|
||||
<<<<<<<< HEAD:dist/houseplan-assets/summary-panel-editor-DoCYqzD1.js
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5c1b1c61b738b69334f9555c60ee17aa874c642234b0dcca6c358552357c0daf";import{b as a,A as e}from"./houseplan-card-yh8-YihB.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-CPSfTezy.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0a76a76ae3d778006d8d7f2d7296a95336a306c743d15efd5166a8249caedeee";import{b as a,A as e}from"./houseplan-card-CfF5g1xu.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-DZOniG9e.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
>>>>>>>> 1ff91b54 (feat(adoption): one owner for config/layout identity and the adoption sequence):dist/houseplan-assets/summary-panel-editor-C77DC5Fh.js
|
||||
========
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="045c3e1a166366a67437f538c473ec2299bdcc82c9d51096ef49b02b7c4d8a9d";import{b as a,A as e}from"./houseplan-card-BKVJQukC.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-CNC8EpID.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
>>>>>>>> c360bcc9 (test(adoption): ownership lint, post-write smoke, harness seam and docs (#500)):dist/houseplan-assets/summary-panel-editor-CtqrpdOR.js
|
||||
<span class="summary-switch-caption"><strong>${b("summary.show_local")}</strong>
|
||||
<small id="summary-local-show-hint">${b("summary.show_local_hint")}</small></span>
|
||||
<input id="summary-local-show" data-summary-local-show type="checkbox"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1365,6 +1365,26 @@ snapshot for every affected space, clears its gestures and geometry history,
|
||||
then best-effort reloads authoritative config. Thus a newer edit made while the
|
||||
rejected request was in flight cannot survive on an unaccepted base (#314).
|
||||
|
||||
**Config/layout identity has one owner** (#500). `src/config-adoption.ts`
|
||||
holds the server config and the device layout together with their revision
|
||||
and content fingerprint; the card exposes `_serverCfg`, `_cfgRev`, `_layout`
|
||||
and `_layoutRev` only as read delegates. The identity changes in exactly
|
||||
three ways — adopting an authoritative response, accepting the reply to our
|
||||
own write (`acceptConfigWrite`, `acceptPairWrite`), restoring the warm cache —
|
||||
and a revision is never taken apart from the body it describes: after
|
||||
`space/delete`, Optimize Undo or Import the revisions come from the re-read
|
||||
`config/get`/`layout/get`, not from the write reply. Every authoritative
|
||||
adoption goes through `adoptAuthoritativeGated`: compare by fingerprint →
|
||||
backdrop readiness (`ContentSigner.prepareImage`) → continuity candidate →
|
||||
adopt → tail. The `reload` profile (initial load, `config_updated`, summary
|
||||
lost-ACK recovery) runs the shared tail (decor assets, initial space, pending
|
||||
nav mode, cache snapshot); the `post-write` profile (the four re-reads after a
|
||||
paired write) ends at adoption and leaves each caller its own tail. Bodies may
|
||||
still be staged locally before a write — that is how the editors work — but
|
||||
only in the files pinned by `test/config-adoption-ownership.test.mjs`, whose
|
||||
counts ratchet down. Feature runtimes see one host method,
|
||||
`_adoptAuthoritative`, instead of the eight steps it replaces.
|
||||
|
||||
**Persisted coordinates have one lattice-aware write boundary** (#291).
|
||||
`canonicalizeConfigGeometry()` / `canonicalizeLayoutGeometry()` /
|
||||
`canonicalizePosition()` own the frontend candidate; mirrored Python functions
|
||||
|
||||
@@ -207,15 +207,16 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
|
||||
* ленивым чанком. Измеренный факт 298 891 Б оставляет 709 Б сверху и 1 291 Б
|
||||
* до нижней границы полосы; общий бюджет 301 066 Б и долг #367 не меняются.
|
||||
*
|
||||
* 2026-09-10, #500: потолок перецентрирован 299 600 → 300 200. Идентичность
|
||||
* 2026-09-10, #500: потолок перецентрирован 299 600 → 300 300. Идентичность
|
||||
* config/layout и единая гейт-последовательность adoption вынесены в
|
||||
* `config-adoption.ts` (+~600 Б gzip: владелец идентичности, делегаты хоста,
|
||||
* post-write профиль для четырёх путей, которые раньше гейт пропускали);
|
||||
* дубли в `_loadFromServer`/`_reloadConfigOnly`/summary recovery сняты.
|
||||
* Измеренный факт 299 577 Б оставляет 623 Б сверху и 1 377 Б до нижней
|
||||
* границы полосы; общий бюджет 301 066 Б и долг #367 не меняются.
|
||||
* `config-adoption.ts` (+~740 Б gzip: владелец идентичности, делегаты хоста
|
||||
* с harness-сеттерами ревизий, post-write профиль для четырёх путей, которые
|
||||
* раньше гейт пропускали); дубли в `_loadFromServer`/`_reloadConfigOnly`/
|
||||
* summary recovery сняты. Измеренный факт 299 718 Б оставляет 582 Б сверху и
|
||||
* 1 418 Б до нижней границы полосы; общий бюджет 301 066 Б и долг #367 не
|
||||
* меняются.
|
||||
*/
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 300_200;
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 300_300;
|
||||
export const INITIAL_VIEW_CEILING_BAND = 2_000;
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: [
|
||||
'confirmedSummaryPanelWriteRecovery', 'summaryPanelEntityIds',
|
||||
'_adoptStructuralResponses', 'LoadedSummaryPanelRuntime',
|
||||
'adoptAuthoritativeGated', 'LoadedSummaryPanelRuntime',
|
||||
],
|
||||
smokes: ['smoke_summary_panel.mjs'],
|
||||
because: 'the #490 production-bundle sequence proves that a lost config/set ACK adopts the '
|
||||
|
||||
@@ -181,6 +181,17 @@ export class MutableConfigAdoption {
|
||||
stageLocalConfig(config: ServerConfig | null): void { this.config = config; }
|
||||
stageLocalLayout(layout: DeviceLayout): void { this.layout = layout; }
|
||||
|
||||
/**
|
||||
* Browser-harness seam: smokes and capture scripts drive revisions and
|
||||
* fingerprints from outside the card (`card._cfgRev = rev`). Production code
|
||||
* never calls this — `test/config-adoption-ownership.test.mjs` pins the only
|
||||
* callers to the card's delegate setters.
|
||||
*/
|
||||
seedIdentity(patch: Partial<Pick<MutableConfigAdoption,
|
||||
'configRev' | 'layoutRev' | 'configFingerprint' | 'layoutFingerprint'>>): void {
|
||||
Object.assign(this, patch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-pair fingerprints with the live bodies. Local edits mutate nested
|
||||
* config/layout before the debounced write; the cache and the rollback of a
|
||||
|
||||
@@ -919,6 +919,11 @@ export class HouseplanCard extends LitElement {
|
||||
public readonly _adoption: ConfigAdoption = createConfigAdoption();
|
||||
private get _layout(): DeviceLayout { return this._adoption.layout; }
|
||||
private set _layout(layout: DeviceLayout) { this._adoption.stageLocalLayout(layout); }
|
||||
// Setters below exist for the browser harness only (smokes seed revisions).
|
||||
private get _layoutRev(): number { return this._adoption.layoutRev; }
|
||||
private set _layoutRev(layoutRev: number) { this._adoption.seedIdentity({ layoutRev }); }
|
||||
private get _layoutContentFingerprint(): string { return this._adoption.layoutFingerprint; }
|
||||
private set _layoutContentFingerprint(layoutFingerprint: string) { this._adoption.seedIdentity({ layoutFingerprint }); }
|
||||
private _serverStorage = false;
|
||||
private _loadOk = false;
|
||||
/** null until config/get answers; then mirrors auth.may_write for this user. */
|
||||
@@ -928,13 +933,15 @@ export class HouseplanCard extends LitElement {
|
||||
private get _serverCfg(): ServerConfig | null { return this._adoption.config; }
|
||||
private set _serverCfg(config: ServerConfig | null) { this._adoption.stageLocalConfig(config); }
|
||||
private get _cfgRev(): number { return this._adoption.configRev; }
|
||||
private set _cfgRev(configRev: number) { this._adoption.seedIdentity({ configRev }); }
|
||||
private get _cfgContentFingerprint(): string { return this._adoption.configFingerprint; }
|
||||
private set _cfgContentFingerprint(configFingerprint: string) { this._adoption.seedIdentity({ configFingerprint }); }
|
||||
private _unsubCfg: (() => void) | null = null;
|
||||
private _unsubLayout: (() => void) | null = null;
|
||||
private _unsubVirtual: (() => void) | null = null;
|
||||
private _liveSyncAttempt: Promise<void> | null = null;
|
||||
private _liveSyncGeneration = 0;
|
||||
private _liveSyncConnection: any = null;
|
||||
private get _layoutRev(): number { return this._adoption.layoutRev; }
|
||||
private _virtualLights: VirtualLightSnapshot = virtualLightSnapshot(null);
|
||||
/** One-deep server snapshot; invalidated by the first later plan edit. */
|
||||
private _canOptimizeUndo = false;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const OWNER = 'src/config-adoption.ts';
|
||||
|
||||
/**
|
||||
* #500 — one owner for config/layout identity.
|
||||
*
|
||||
* Revision and fingerprint of the server config and of the device layout are
|
||||
* written by `ConfigAdoption` and nowhere else: a revision taken apart from
|
||||
* the body it belongs to is how #490 F1 lost a concurrent edit. Bodies may
|
||||
* still be staged locally before a write (`_serverCfg = candidate`,
|
||||
* `_layout = { ...pos }`) — that is the editor's job — but only in the files
|
||||
* pinned below, and the counts are a ratchet: they may go down, never up.
|
||||
*/
|
||||
|
||||
const tsFiles = (dir) => readdirSync(dir).flatMap((name) => {
|
||||
const path = join(dir, name);
|
||||
if (statSync(path).isDirectory()) return tsFiles(path);
|
||||
return name.endsWith('.ts') ? [path] : [];
|
||||
});
|
||||
const sources = tsFiles(join(repoRoot, 'src'))
|
||||
.map((path) => ({ file: relative(repoRoot, path).replaceAll('\\', '/'), text: readFileSync(path, 'utf8') }))
|
||||
.filter(({ file }) => file !== OWNER);
|
||||
|
||||
const IDENTITY_WRITE = /\b(?:_cfgRev|_layoutRev|_cfgContentFingerprint|_layoutContentFingerprint)\s*(?:=(?!=)|\+\+|--|\+=|-=)/g;
|
||||
const BODY_WRITE = {
|
||||
_serverCfg: /\b_serverCfg\s*=(?!=)/g,
|
||||
_layout: /\b_layout\s*=(?!=)/g,
|
||||
};
|
||||
|
||||
/**
|
||||
* Local staging of bodies before a write. Reading these numbers: the editor
|
||||
* replaces the working config for settings dialogs and marker saves; the card
|
||||
* stages device positions (`applyDevicePlacement`). Lowering a count is a
|
||||
* win to record here; raising one is a review finding.
|
||||
*/
|
||||
const BODY_STAGING_ALLOWLIST = {
|
||||
'src/houseplan-editor-runtime.ts': { _serverCfg: 5, _layout: 5 },
|
||||
'src/houseplan-card.ts': { _serverCfg: 3, _layout: 9 },
|
||||
};
|
||||
|
||||
const count = (text, re) => (text.match(re) || []).length;
|
||||
|
||||
test('AC1: revision and fingerprint identity is written only by the owner', () => {
|
||||
const offenders = sources
|
||||
.flatMap(({ file, text }) => (text.match(IDENTITY_WRITE) || []).map((hit) => `${file}: ${hit.trim()}`));
|
||||
assert.deepEqual(offenders, [], 'a revision belongs to the body it came with — go through ConfigAdoption');
|
||||
});
|
||||
|
||||
test('AC1: body staging outside the owner stays within the pinned files and counts (ratchet)', () => {
|
||||
const problems = [];
|
||||
for (const { file, text } of sources) {
|
||||
const pinned = BODY_STAGING_ALLOWLIST[file];
|
||||
for (const [field, re] of Object.entries(BODY_WRITE)) {
|
||||
const actual = count(text, re);
|
||||
const allowed = pinned?.[field] ?? 0;
|
||||
if (actual > allowed) problems.push(`${file}: ${field} assigned ${actual}×, allowed ${allowed}`);
|
||||
else if (actual < allowed) problems.push(`${file}: ${field} assigned ${actual}×, pin ${allowed} — lower the pin, keep the win`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(problems, []);
|
||||
});
|
||||
|
||||
test('AC1: the harness seam `seedIdentity` is reachable only through the card delegate setters', () => {
|
||||
const callers = sources.filter(({ text }) => text.includes('seedIdentity('));
|
||||
assert.deepEqual(callers.map(({ file }) => file), ['src/houseplan-card.ts']);
|
||||
const lines = callers[0].text.split('\n').filter((line) => line.includes('seedIdentity('));
|
||||
assert.equal(lines.length, 4);
|
||||
for (const line of lines) assert.match(line.trim(), /^private set _(?:cfgRev|layoutRev|cfgContentFingerprint|layoutContentFingerprint)\(/);
|
||||
});
|
||||
|
||||
test('AC2: the former host seam is gone and every module adopts through the one gated entry', () => {
|
||||
const seam = sources.filter(({ text }) => /\b_adoptStructuralResponses\b/.test(text)).map(({ file }) => file);
|
||||
assert.deepEqual(seam, [], '_adoptStructuralResponses must not come back as a host method');
|
||||
const adopters = sources.filter(({ text }) => /_adoptAuthoritative\(\{/.test(text)).map(({ file }) => file).sort();
|
||||
assert.deepEqual(adopters, [
|
||||
'src/houseplan-card.ts',
|
||||
'src/houseplan-editor-runtime.ts',
|
||||
'src/houseplan-onboarding-runtime.ts',
|
||||
'src/summary-panel-runtime-loaded.ts',
|
||||
]);
|
||||
const owner = readFileSync(join(repoRoot, OWNER), 'utf8');
|
||||
for (const reason of ['structural-response', 'config-reload', 'summary-recovery', 'space-delete', 'optimize-undo', 'import-apply']) {
|
||||
assert.ok(owner.includes(`'${reason}'`), `${reason} is a declared adoption reason`);
|
||||
assert.ok(sources.some(({ text }) => text.includes(`reason: '${reason}'`)), `${reason} has a caller`);
|
||||
}
|
||||
});
|
||||
|
||||
test('AC2: feature-runtime host contracts expose one adoption method, not its eight steps', () => {
|
||||
const summaryHost = readFileSync(join(repoRoot, 'src/summary-panel-host.ts'), 'utf8');
|
||||
for (const step of [
|
||||
'_beginContinuityCandidate', '_syncDecorAssets', '_adoptInitialSpace', '_resumePendingNavMode',
|
||||
'_candidateBackdrop', '_scheduleLoadRetry', '_signer', '_continuity', '_cfgContentFingerprint',
|
||||
]) {
|
||||
assert.ok(!summaryHost.includes(step), `SummaryPanelHost must not expose ${step}`);
|
||||
}
|
||||
assert.match(summaryHost, /_adoptAuthoritative\(input: GatedAdoptionInput\)/);
|
||||
const editorPort = readFileSync(join(repoRoot, 'src/houseplan-editor-runtime.ts'), 'utf8');
|
||||
assert.match(editorPort, /readonly _cfgRev: number;/);
|
||||
assert.match(editorPort, /readonly _layoutRev: number;/);
|
||||
assert.match(editorPort, /readonly _adoption: ConfigAdoption;/);
|
||||
const rollbackCallers = sources.filter(({ text }) => /(?<![_\w])rollbackOptimistic\(/.test(text)).map(({ file }) => file).sort();
|
||||
assert.deepEqual(rollbackCallers, ['src/houseplan-card.ts'], 'only the host wrapper reaches the owner\'s rollback');
|
||||
});
|
||||
@@ -23,7 +23,7 @@ const CAPS = {
|
||||
// model and rendering themselves remain in dedicated modules.
|
||||
// #500 moved config/layout identity and the adoption sequence into
|
||||
// src/config-adoption.ts; the ratchet keeps that win.
|
||||
'src/houseplan-card.ts': 13600,
|
||||
'src/houseplan-card.ts': 13650,
|
||||
// #478 removed the persisted room-draft editor branch. Keep that reduction.
|
||||
// #485 keeps its large setup surface in editors/radar-section.ts; these are
|
||||
// only the dialog state/save seam and the thin lazy-render adapter.
|
||||
|
||||
Reference in New Issue
Block a user