Files
houseplan-card/dist/houseplan-panel.js
T
Claude 462b56453c fix: the authoritative adoption keeps its own task (#520)
The r1 diagnosis was wrong, and the measurement in the code review proved
it: removing the two declarations from `static properties` left the cold
start at 19 update cycles, 4 model builds and 4 config epochs, exactly the
numbers of the bug. Lit's forced first-update change does mark `_serverCfg`
changed, but at that moment the body and `_cfgEpochPreservedConfig` are both
null, `preserveGeometry` is true and the epoch does not move. The comment
above `static properties` now says that; the declaration still stays out,
because two owners of one reactivity is what #500 removed.

The real cause is the `await`. Before #500 everything from
`_adoptStructuralResponses` to the end of the load ran in one task: the
adopted bodies, `_adoptInitialSpace`, the viewport restore, `_loadOk`, and
the device seeding — whose `_syncNewDevices`/`_seedHiddenDevices` write the
config back — all landed in a single Lit update. #500 made the adoption an
async sequence, so the caller resumes one microtask later, after Lit has
already painted the adopted config; the seeding writes then arrive as a
second config epoch, a second model build and a second paint of a 60-room
house.

`GatedAdoptionInput` gains `afterAdopt`, the mirror of `beforeAdopt`: it
runs synchronously at the end of the sequence, before the promise resolves.
`_loadFromServer` moves the viewport restore, `_loadOk` and the device
rebuild into it — `_syncNewDevices` refuses to write before `_loadOk`, so
the order inside the hook matters — and the load tail now rebuilds devices
only when nothing was adopted. `_reloadConfigOnly` takes the same route.

Measured with the project's own runner, 7 samples per profile, base
`a44fbd37` against this tree (Chromium 152, sandbox):

  interaction  modelReadyMs 761.3 ≤ 950.56 (base 731.2)
               firstStableRenderMs 2567.2 ≤ 3000 (base 2542.7)
               longTask.maxSingleMs 690 ≤ 921 · cache.entries.cleanFloor 100
  isometric    modelReadyMs 1252.9 ≤ 1499.76 (base 1249.8)
               firstStableRenderMs 1378 ≤ 1610.16 (base 1341.8)

Boot diagnostics on both trees: 18 update cycles, 3 model builds, 3 config
epochs, with the same epoch trace — the candidate is no longer
distinguishable from the base.

Witnesses. `config-adoption.test.mjs` queues a microtask at the start of
the adoption and pins that `afterAdopt` runs before it — the probe fails the
moment the hook crosses an await; `config-adoption-ownership.test.mjs` pins
the wiring in the card and the hook's place in the sequence. Mutants
`adoption-tail-defers-caller-hook` (defers the hook by one microtask) and
`authoritative-load-seeds-devices-after-the-await` (drops the rebuild from
the hook) redden them.

The initial View graph grows 40 B gzip, so the #438 ceiling is recentred
300 300 → 300 400 with the usual dated note; measured 299 812 B keeps 588 B
above and 1 412 B below the band. The overall 301 066 B budget and the #367
headroom debt are untouched.

Issue: #520
User-Visible: no
2026-09-10 20:12:13 +03:00

2 lines
6.0 KiB
JavaScript

globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2e6f7d70c797ae4e865b10859c77cc3380a432272a975b8f35b51234ae3cfe62";try{await import("./houseplan-card.js")}catch(e){if(!customElements.get("houseplan-panel")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-panel",class extends HTMLElement{connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;min-height:100%;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 houseplan-panel.js: the card entry is unavailable",e)}const e=["panel","hass","narrow","route"];class n extends HTMLElement{constructor(){super(),this._narrow=!1,this.attachShadow({mode:"open"}),this._adoptPreUpgradeProperties()}connectedCallback(){this._adoptPreUpgradeProperties(),this._ensureShell()}_adoptPreUpgradeProperties(){for(const n of e){if(!Object.prototype.hasOwnProperty.call(this,n))continue;const e=this[n];delete this[n],this[n]=e}}get hass(){return this._hass}set hass(e){this._hass=e,this._ensureShell(),this._card&&(this._card.hass=e),this._syncMenuLabel()}get narrow(){return this._narrow}set narrow(e){this._narrow=Boolean(e),this.toggleAttribute("narrow",this._narrow),this._card&&(this._card.narrow=this._narrow)}get route(){return this._route}set route(e){this._route=e}get panel(){return this._panel}set panel(e){this._panel=e}_ensureShell(){if(this._card||!this.shadowRoot)return;const e=document.createElement("style");e.textContent="\n :host {\n display: block;\n width: 100%;\n /* HA mounts a custom panel inside <ha-panel-custom>, a block with\n * safe-area padding and NO height, so a percentage here resolves to\n * auto and the stage collapses to 0 px (#488). Take the viewport\n * like HA's own iframe panels do, minus the insets HA pads with. */\n height: 100vh;\n height: calc(100dvh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px));\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n color: var(--primary-text-color);\n background: var(--primary-background-color);\n }\n .page {\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n overflow: hidden;\n }\n .appbar {\n box-sizing: border-box;\n min-width: 0;\n min-height: var(--header-height, 56px);\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 0 16px;\n background: var(--app-header-background-color, var(--primary-background-color));\n color: var(--app-header-text-color, var(--primary-text-color));\n border-bottom: 1px solid var(--divider-color);\n z-index: 1;\n }\n .toolbar {\n min-width: 0;\n width: 100%;\n display: flex;\n align-items: center;\n gap: 12px;\n }\n .menu {\n box-sizing: border-box;\n width: 44px;\n height: 44px;\n min-width: 44px;\n min-height: 44px;\n margin: 0;\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 50%;\n color: inherit;\n background: transparent;\n cursor: pointer;\n }\n .menu:focus-visible {\n outline: 2px solid var(--primary-color);\n outline-offset: 2px;\n }\n @media (hover: hover) {\n .menu:hover { background: var(--secondary-background-color); }\n }\n .menu ha-icon { --mdc-icon-size: 24px; }\n .title {\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 20px;\n font-weight: 500;\n line-height: 1.2;\n }\n .title ha-icon {\n --mdc-icon-size: 24px;\n color: var(--primary-color);\n }\n .content {\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n }\n houseplan-card {\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n }\n ";const n=document.createElement("div");n.className="page";const t=document.createElement("header");t.className="appbar";const a=document.createElement("div");a.className="toolbar",a.setAttribute("role","toolbar");const o=document.createElement("button");o.className="menu",o.dataset.hp="panel-menu",o.type="button",o.innerHTML='<ha-icon icon="mdi:menu"></ha-icon>',o.addEventListener("click",()=>{o.dispatchEvent(new CustomEvent("hass-toggle-menu",{bubbles:!0,composed:!0}))}),this._menuButton=o;const i=document.createElement("div");i.className="title",i.dataset.hp="panel-title",i.setAttribute("role","heading"),i.setAttribute("aria-level","1"),i.innerHTML='<ha-icon icon="mdi:floor-plan"></ha-icon><span>House Plan</span>',a.append(o,i),t.append(a);const r=document.createElement("main");r.className="content";const s=document.createElement("houseplan-card");s.panelHost=!0,s.narrow=this._narrow,s.setConfig({type:"custom:houseplan-card"}),void 0!==this._hass&&(s.hass=this._hass),r.append(s),n.append(t,r),this.shadowRoot.append(e,n),this._card=s,this._syncMenuLabel()}_syncMenuLabel(){if(!this._menuButton)return;const e=this._hass?.localize?.("ui.common.menu")||this._hass?.localize?.("ui.panel.lovelace.menu")||"Menu";this._menuButton.setAttribute("aria-label",e),this._menuButton.title=e}}customElements.get("houseplan-panel")||customElements.define("houseplan-panel",n);export{n as HouseplanPanel};