Release v1.74.0 — слой пространства с ключом

Догоняем main после #534: смена пространства больше не сверяет двести
маркеров поэлементно, чтобы затем всё выбросить.

Issue: #534
User-Visible: no
Release: v1.74.0
This commit is contained in:
Codex
2026-09-11 22:52:06 +03:00
63 changed files with 2246 additions and 663 deletions
@@ -1,109 +1,109 @@
{
"schema": 1,
"fingerprint": "dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039",
"fingerprint": "b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 287242,
"initialViewGzipBytes": 287302,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-card.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 289548,
"initialPanelGzipBytes": 289606,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2306,
"initialPanelOnlyGzipBytes": 2304,
"lazyFiles": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/de-BAfQ3noX.js",
"houseplan-assets/editor-Ca9vnH-K.js",
"houseplan-assets/fr-a4v8Ohtu.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js",
"houseplan-assets/hp-zigbee-topology-overlay-B_T0ilCj.js",
"houseplan-assets/iso-scene-render-DtlvcvOT.js",
"houseplan-assets/live-interaction-runtime-CjYDG1SP.js",
"houseplan-assets/pdf-export-CY9TOlZ5.js",
"houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js",
"houseplan-assets/zigbee-topology-runtime-Cez46hoy.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/de-Cl_N0-YX.js",
"houseplan-assets/editor-DAm0GfXr.js",
"houseplan-assets/fr-D490TXwk.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js",
"houseplan-assets/hp-zigbee-topology-overlay-xOc6gz5D.js",
"houseplan-assets/iso-scene-render-B6PFBRuR.js",
"houseplan-assets/live-interaction-runtime-uOonwwaS.js",
"houseplan-assets/pdf-export-CYH2TEQI.js",
"houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js",
"houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js"
],
"lazyGzipBytes": 417431,
"lazyGzipBytes": 417421,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/editor-Ca9vnH-K.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"houseplan-assets/zigbee-topology-DitREaKv.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/editor-DAm0GfXr.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js"
],
"lazyEditorGzipBytes": 204999,
"lazyEditorGzipBytes": 204997,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js"
],
"lazyOnboardingGzipBytes": 13916,
"lazyOnboardingGzipBytes": 13918,
"lazyLocaleFiles": [
"houseplan-assets/de-BAfQ3noX.js",
"houseplan-assets/fr-a4v8Ohtu.js"
"houseplan-assets/de-Cl_N0-YX.js",
"houseplan-assets/fr-D490TXwk.js"
],
"lazyLocaleGzipBytes": 54820,
"lazyLocaleGzipBytes": 54821,
"lazyIsometricFiles": [
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/iso-scene-render-DtlvcvOT.js"
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/iso-scene-render-B6PFBRuR.js"
],
"lazyIsometricGzipBytes": 13460,
"lazyIsometricGzipBytes": 13456,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js"
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js"
],
"lazyFurnitureArtGzipBytes": 10254,
"lazyFurnitureArtGzipBytes": 10251,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/pdf-export-CY9TOlZ5.js"
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/pdf-export-CYH2TEQI.js"
],
"lazyPdfGzipBytes": 120128,
"lazyPdfGzipBytes": 120123,
"files": [
{
"path": "houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"sha256": "ce22e7599ea095db52bf13da52204107ed2d535fb57f69766e3669dfea1c9ef2",
"path": "houseplan-assets/backdrop-pick-BVE5pvXV.js",
"sha256": "04f4f6ebe74df3384c918202679b3ff9380729b553ff798dafe33ba5231eaaa4",
"rawBytes": 19956,
"gzipBytes": 6879,
"gzipBytes": 6880,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BAfQ3noX.js",
"sha256": "38d0ffe6551c85bc7a5686943679357a726928644136850dc646e0cdd5713bab",
"path": "houseplan-assets/de-Cl_N0-YX.js",
"sha256": "2ea60918c00765be8acb766982dcc9b58715271c62f2eef3dabc07218dd70f64",
"rawBytes": 91402,
"gzipBytes": 27694,
"gzipBytes": 27695,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Ca9vnH-K.js",
"sha256": "fd8b9731bc4a417cb0545e06677c836e98fa59a7b28e7cf0345d87c25f4aaed7",
"path": "houseplan-assets/editor-DAm0GfXr.js",
"sha256": "2524ee570c6d20502ae83cbacddc596882d293b27f7943ae59521c7b1923e796",
"rawBytes": 3832,
"gzipBytes": 1583,
"gzipBytes": 1586,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-a4v8Ohtu.js",
"sha256": "ccc6ccd7d3736f7567080f000de2dd04ecf60989163720e46d11063cd3ace9ee",
"path": "houseplan-assets/fr-D490TXwk.js",
"sha256": "8ff8d7ac1efd3359263752a7e11a26b3c34bc12ae874108c684a07ea2ff9590f",
"rawBytes": 93667,
"gzipBytes": 27126,
"isEntry": false,
@@ -111,199 +111,199 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"sha256": "d8e59b11da6c8f4a665b33cc6d92e9eb9e2e9ef94fed1071ba5988ce5c98e438",
"path": "houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"sha256": "016eabf8c967f1a22d567a35f83102a80b9d14ce864162308bb833855e440c40",
"rawBytes": 33655,
"gzipBytes": 10254,
"gzipBytes": 10251,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/guard-BYWHhzjw.js",
"sha256": "b81804f104d25d22dbc909341c48821dbe950aad94054b9512f5322a04fdfc8b",
"path": "houseplan-assets/guard-zx2aaPuf.js",
"sha256": "3455ec5c4d0ea4b31e517eb4aa029de858d0078fc6e59d49737612224440a98f",
"rawBytes": 518,
"gzipBytes": 369,
"gzipBytes": 371,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DEVy5dLN.js",
"sha256": "d8cf65be7367f51ed4c8069825965c002036ec16321e3dc83dce1638ec7d2565",
"rawBytes": 1020996,
"gzipBytes": 286437,
"path": "houseplan-assets/houseplan-card-bOJWMeFY.js",
"sha256": "69a22d0bc488c22923dcc434d2e77fa2d9f12eb75a37c4b388507de7a4d9ce9d",
"rawBytes": 1021194,
"gzipBytes": 286498,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BAfQ3noX.js",
"houseplan-assets/editor-Ca9vnH-K.js",
"houseplan-assets/fr-a4v8Ohtu.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js",
"houseplan-assets/hp-zigbee-topology-overlay-B_T0ilCj.js",
"houseplan-assets/iso-scene-render-DtlvcvOT.js",
"houseplan-assets/live-interaction-runtime-CjYDG1SP.js",
"houseplan-assets/pdf-export-CY9TOlZ5.js",
"houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js"
"houseplan-assets/de-Cl_N0-YX.js",
"houseplan-assets/editor-DAm0GfXr.js",
"houseplan-assets/fr-D490TXwk.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js",
"houseplan-assets/hp-zigbee-topology-overlay-xOc6gz5D.js",
"houseplan-assets/iso-scene-render-B6PFBRuR.js",
"houseplan-assets/live-interaction-runtime-uOonwwaS.js",
"houseplan-assets/pdf-export-CYH2TEQI.js",
"houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"sha256": "b26691e5ac6338578a23ab3d6c0a774e36c4e708f2bb4a48894ab7e4b0ca7c96",
"path": "houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"sha256": "7433aa76bf0dd431661b9ca16546c81599493662392375ea421940a13e6c865f",
"rawBytes": 668813,
"gzipBytes": 178484,
"gzipBytes": 178479,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-Cez46hoy.js"
"houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js",
"sha256": "e1fc28837937564eee4a602230cdaf3fbabe35703a7ecda43ce74def1159d0b7",
"path": "houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js",
"sha256": "66085f37bd788c2f5e2d50d580655e93325a6b707b003500b953fdf815b926d8",
"rawBytes": 28287,
"gzipBytes": 7037,
"gzipBytes": 7038,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-B_T0ilCj.js",
"sha256": "d9f0676bc0034a3baed8c9108f8799aa261cbabbef86929ef812cd90e0e9e302",
"path": "houseplan-assets/hp-zigbee-topology-overlay-xOc6gz5D.js",
"sha256": "c1007a28222b185c910284669d96f6dc941a2d7606b3194086f8c4fb29cdfd6f",
"rawBytes": 10756,
"gzipBytes": 3684,
"gzipBytes": 3683,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js",
"houseplan-assets/zigbee-topology-runtime-Cez46hoy.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js",
"houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-DtlvcvOT.js",
"sha256": "ad3e8c6b41caf6edc5e3b259dcffa3bff98c0110ae80cbd3bf736d5eccfb7d0f",
"path": "houseplan-assets/iso-scene-render-B6PFBRuR.js",
"sha256": "aa5903e34a2fd82469d195a19e4288c07b29d20130a9f99ed649903c2c6d6353",
"rawBytes": 38562,
"gzipBytes": 13091,
"gzipBytes": 13085,
"isEntry": false,
"imports": [
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-CjYDG1SP.js",
"sha256": "886197ec8207c347463a743dd3a7a2f95bb05efbe4545332a41c12891cc3242e",
"path": "houseplan-assets/live-interaction-runtime-uOonwwaS.js",
"sha256": "b26b6e1cadb17533b49d7d824cccc4a649def8557953e632b8075fa2da358594",
"rawBytes": 6032,
"gzipBytes": 2433,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CY9TOlZ5.js",
"sha256": "0fa7863db13ffe8d654f35cf25222639be2f6c4ed018941feffde0503da2b425",
"path": "houseplan-assets/pdf-export-CYH2TEQI.js",
"sha256": "1bf1783f9f3f80b37d38b5000a831860334f1a4cb1aacb58dc172b6c9f25f65b",
"rawBytes": 236244,
"gzipBytes": 109874,
"gzipBytes": 109872,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DeBQ1tcV.js",
"sha256": "b1bbc6089d46f90c81c295e0305374ca2f2c5fca0322f27d4e9ecb9bb76ead28",
"path": "houseplan-assets/summary-panel-editor-TASGFcvs.js",
"sha256": "9e4e2bd331c2f5fdfb6cc6719f09795c5556d712fd961e2cd18d561f2dfee7e2",
"rawBytes": 14824,
"gzipBytes": 3401,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-B3ruGJoU.js",
"sha256": "d68458815e4a7772cd87bf55f1448ba1794c54b1e6c749825f0672952051a93c",
"path": "houseplan-assets/summary-panel-metrics-bIh_Q9NB.js",
"sha256": "e50c875e83ee220fc99121b013f04f3417b8c1412d2d08265b94f4d2e3c74f57",
"rawBytes": 2610,
"gzipBytes": 1306,
"gzipBytes": 1308,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js",
"sha256": "d7d78a52ff91ce0dd1d07eb8e670e931814ef71c969f67069485d1313adeafe4",
"path": "houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js",
"sha256": "78d50efb02e10eaeb8a5c4d299e98f298606d1d95a419e6bfdfdf8dc37ff36a9",
"rawBytes": 72272,
"gzipBytes": 19728,
"gzipBytes": 19727,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DeBQ1tcV.js",
"houseplan-assets/summary-panel-metrics-B3ruGJoU.js"
"houseplan-assets/summary-panel-editor-TASGFcvs.js",
"houseplan-assets/summary-panel-metrics-bIh_Q9NB.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-DitREaKv.js",
"sha256": "d29bb086e6862441a6115d431ed71e4ee0b96201a10bdfedb93d3f11280264ca",
"path": "houseplan-assets/zigbee-topology-70mzkKN3.js",
"sha256": "789d0ec02c2b0133838b379975944bb2730ee6c4679c9262000861d8457535a5",
"rawBytes": 18961,
"gzipBytes": 7430,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-Cez46hoy.js",
"sha256": "5bb047f1c003a211e0a57f2fa03d37e968144ca741d99b2f3e486ee70e4aa130",
"path": "houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js",
"sha256": "04c8c7571c42d0659a412b4d560d3dbc504209631591f58ab7c86afd3580fedf",
"rawBytes": 3941,
"gzipBytes": 1765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "2cf259caacb83ef37c3e5e743f88bcd61a6f48131af71fa565528f409b2f1f82",
"sha256": "6bf9432b3ce24211e3771939aca869d04bf03e4412c118129b3559d9561d6923",
"rawBytes": 1205,
"gzipBytes": 805,
"gzipBytes": 804,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "02fa46808fbf68c9245ec2b518ec2047953678b13b054a923259f1a8d9e9cb9a",
"sha256": "fbdee8bf86166bcbca6e899b1ffb00f840eda83bb4a7edb66390bd08982c5a7b",
"rawBytes": 6124,
"gzipBytes": 2306,
"gzipBytes": 2304,
"isEntry": true,
"imports": [
"houseplan-card.js"
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";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-DEVy5dLN.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__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";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-bOJWMeFY.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}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{dS as r,dT as t,dU as s}from"./houseplan-card-DEVy5dLN.js";const e={},a=r(class extends t{constructor(){super(...arguments),this.ot=e}render(r,t){return t()}update(r,[t,e]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return s}else if(this.ot===t)return s;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,e)}});export{a as i};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{dS as r,dT as t,dU as a}from"./houseplan-card-bOJWMeFY.js";const s={},e=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 a}else if(this.ot===t)return a;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{e as i};
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{b as a,A as e}from"./houseplan-card-DEVy5dLN.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-CB4AHgRN.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__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{b as a,A as e}from"./houseplan-card-bOJWMeFY.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-cFcfVYdb.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">
<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"
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{e9 as e,L as t,a2 as n,J as i,an as o,N as r,bo as s,bn as a,a1 as c,a0 as l,ax as d,ea as u,eb as f,bq as m,ec as g}from"./houseplan-card-DEVy5dLN.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=d(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const r=i.binding.slice(0,o),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,c="entity"===r?e.registry.entities?.[s]:null,l="device"===r?s:c?.device_id,d="device"===r?a?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===r&&n.liveEntities.has(s))&&t.add(l)}return t}function y(e,t){return e?.length?t?.length?g(e,t):e:t}function b(e,t){const n=c(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,r);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:l(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*_(c,l,d=b){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||a;v=y(v,s(c,m.physicalBodies)),yield}const b=m.cellCm/o;u+=a(v)*b*b/1e4}return u}catch{return null}}function h(e,t,n=b){const i=_(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function p(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:m(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function S(e,t){const n=e?.states?.[t];if(!n)return null;const i=u(e,t);return i?f(i,String(n.attributes?.unit_of_measurement||"")):null}export{_ as cleanFloorAreaSteps,v as representedHaDeviceIds,b as spaceWallGeometry,S as summaryEntityValue,p as summarySystemValue,h as totalCleanFloorAreaM2};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{e9 as e,L as t,a2 as n,J as i,an as o,N as a,bo as r,bn as s,a1 as c,a0 as l,ax as d,ea as u,eb as f,bq as m,ec as g}from"./houseplan-card-bOJWMeFY.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=d(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const a=i.binding.slice(0,o),r=i.binding.slice(o+1),s="device"===a?e.registry.devices?.[r]:null,c="entity"===a?e.registry.entities?.[r]:null,l="device"===a?r:c?.device_id,d="device"===a?s?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===a&&n.liveEntities.has(r))&&t.add(l)}return t}function y(e,t){return e?.length?t?.length?g(e,t):e:t}function _(e,t){const n=c(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,a);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:l(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,a)}}function*h(c,l,d=_){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const s=t(e);if(!s)continue;const c=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,a,g.roomGeom,g.multiWallNodes)||s;v=y(v,r(c,m.physicalBodies)),yield}const _=m.cellCm/o;u+=s(v)*_*_/1e4}return u}catch{return null}}function p(e,t,n=_){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function b(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:m(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function S(e,t){const n=e?.states?.[t];if(!n)return null;const i=u(e,t);return i?f(i,String(n.attributes?.unit_of_measurement||"")):null}export{h as cleanFloorAreaSteps,v as representedHaDeviceIds,_ as spaceWallGeometry,S as summaryEntityValue,b as summarySystemValue,p as totalCleanFloorAreaM2};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{n as t,b as n}from"./zigbee-topology-DitREaKv.js";import{B as e}from"./houseplan-card-DEVy5dLN.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function m(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,m=null;const y=new Promise(t=>{u=t}),b=new Promise((t,n)=>{f=t,m=n}),w=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&w.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&m?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&w.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(b,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of w)try{t()}catch{}}})}export{f as readZhaTopology,m as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{n as t,b as n}from"./zigbee-topology-70mzkKN3.js";import{B as e}from"./houseplan-card-bOJWMeFY.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function m(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,m=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,m=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&m?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,m as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";try{await import("./houseplan-assets/houseplan-card-DEVy5dLN.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).":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-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 houseplan-card.js: the implementation chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";try{await import("./houseplan-assets/houseplan-card-bOJWMeFY.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).":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-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 houseplan-card.js: the implementation chunk is unavailable",e)}
File diff suppressed because one or more lines are too long
+141
View File
@@ -0,0 +1,141 @@
// Стоимость растеризации дневного цикла — свидетель #532.
//
// Владелец: «в Firefox план тормозит; со статичным цветом фона тормоза
// пропадают». Профиль подтвердил: наш JS спал 89 % времени, а в GPU-процессе
// на кадр уезжало 23 МБ текстур. Виновником оказалась не сцена плана, а
// тройной drop-shadow внешнего контура: группа .hp-paperg несёт одни бумажные
// силуэты и при ховере и панораме не меняется, но фильтр жил в общем слое
// плана — и каждая перерисовка плана заново прогоняла три прохода размытия по
// габариту всего листа. Лечится одной подсказкой will-change: filter, которая
// уводит отфильтрованную бумагу в свой композиционный слой.
//
// Смок проверяет две вещи, и вторая — не время, а ОТНОШЕНИЕ: абсолютные
// миллисекунды зависят от машины и раннера, отношение «дневной цикл к
// статичному фону» — нет. Оба замера идут в одном процессе, на одной странице,
// подряд, поэтому делят между собой и прогрев, и загрузку машины.
//
// Замер аналитики на панораме демо-стенда: 7.9 без подсказки, 0.26 с ней.
// Порог 2.0 стоит примерно посередине по логарифму и даёт запас в обе стороны.
//
// Движок здесь Chromium — он же движок CI и golden. Приговор по Gecko даёт
// профиль Firefox с машины владельца: подсказка это совет движку, и Gecko
// вправе отклонить её по бюджету памяти (layout.css.will-change.budget).
// Смок нужен не вместо того профиля, а чтобы правку нельзя было молча снять.
import { launch, check, finish } from './serve.mjs';
const PAN_STEPS = 60;
const RATIO_CEILING = 2.0;
const { page, browser } = await launch({ width: 1400, height: 900 }, 1);
const cdp = await page.context().newCDPSession(page);
const setBackground = async (mode) => {
await page.evaluate(async (mode) => {
const card = window.__card;
card._serverCfg.settings = { ...(card._serverCfg.settings || {}), bg_mode: mode };
// Пространство со своим bg_mode переопределяет глобальный — снимаем, иначе
// смок мерил бы не то, что переключил.
for (const space of card._serverCfg.spaces) {
space.settings = { ...(space.settings || {}) };
delete space.settings.bg_mode;
}
card.hass = { ...card.hass, states: { ...card.hass.states, 'sun.sun': {
entity_id: 'sun.sun', state: 'above_horizon',
attributes: { azimuth: 180, elevation: 40, rising: false },
} } };
card._cfgRev = (card._cfgRev || 0) + 1;
card.requestUpdate();
await card.updateComplete;
}, mode);
// Переход фильтра длится 1100 мс: замер до его конца мерил бы анимацию.
await page.waitForTimeout(1400);
};
const outlineStyle = () => page.evaluate(() => {
const root = window.__card.shadowRoot || window.__card.renderRoot;
const stage = root.querySelector('.stage');
const group = root.querySelector('.hp-paperg');
if (!group) return null;
const style = getComputedStyle(group);
return {
daycycle: stage.classList.contains('daycycle'),
filter: style.filter,
willChange: style.willChange,
};
});
const stageBox = () => page.evaluate(() => {
const root = window.__card.shadowRoot || window.__card.renderRoot;
const rect = root.querySelector('.stage').getBoundingClientRect();
return { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
});
/** Одна панорама с зажатой кнопкой; возвращает суммарный RasterTask в мс. */
const measurePan = async () => {
const box = await stageBox();
const events = [];
const collect = ({ value }) => events.push(...value);
cdp.on('Tracing.dataCollected', collect);
await cdp.send('Tracing.start', {
traceConfig: { includedCategories: ['disabled-by-default-devtools.timeline'] },
transferMode: 'ReportEvents',
});
const cx = box.x + box.w / 2, cy = box.y + box.h / 2;
await page.mouse.move(cx, cy);
await page.mouse.down();
for (let step = 0; step < PAN_STEPS; step += 1) {
await page.mouse.move(cx + Math.sin(step / 6) * 140, cy + Math.cos(step / 9) * 90);
// Два кадра на шаг: первый ставит кадр в очередь, второй ждёт, пока он
// действительно отрисуется. Иначе замер считает не работу, а очередь.
await page.evaluate(() => new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(resolve));
}));
}
await page.mouse.up();
await page.waitForTimeout(200);
const complete = new Promise((resolve) => cdp.once('Tracing.tracingComplete', resolve));
await cdp.send('Tracing.end');
await complete;
cdp.off('Tracing.dataCollected', collect);
let micros = 0, tasks = 0;
for (const event of events) {
if (event.name !== 'RasterTask' || event.ph !== 'X' || typeof event.dur !== 'number') continue;
micros += event.dur;
tasks += 1;
}
return { ms: micros / 1000, tasks };
};
const out = {};
// 1. Подсказка стоит там и только там, где стоит фильтр.
await setBackground('daynight');
const dayStyle = await outlineStyle();
out.dayCycleClassOn = dayStyle?.daycycle === true;
out.dayCycleOutlineFiltered = /drop-shadow/.test(dayStyle?.filter || '');
out.dayCycleOutlinePromoted = /filter/.test(dayStyle?.willChange || '');
await setBackground('static');
const staticStyle = await outlineStyle();
out.staticClassOff = staticStyle?.daycycle === false;
out.staticOutlineUnfiltered = (staticStyle?.filter || 'none') === 'none';
// Статичный фон не платит за чужую подсказку ни слоем, ни памятью.
out.staticOutlineNotPromoted = !/filter/.test(staticStyle?.willChange || '');
// 2. Отношение растеризации на настоящей панораме.
const staticPan = await measurePan();
await setBackground('daynight');
const dayPan = await measurePan();
out.rasterTasksObserved = staticPan.tasks > 0 && dayPan.tasks > 0;
const ratio = staticPan.ms > 0 ? dayPan.ms / staticPan.ms : Infinity;
out.rasterRatioWithinBudget = ratio <= RATIO_CEILING;
console.log(JSON.stringify({
staticPanMs: Number(staticPan.ms.toFixed(1)), staticPanTasks: staticPan.tasks,
dayCyclePanMs: Number(dayPan.ms.toFixed(1)), dayCyclePanTasks: dayPan.tasks,
ratio: Number(ratio.toFixed(2)), ceiling: RATIO_CEILING,
}, null, 1));
for (const [name, value] of Object.entries(out)) check(name, value);
await finish(browser, out);
+56 -1
View File
@@ -148,7 +148,62 @@ const res = await page.evaluate(async () => {
const onRealChange = runningAnimations();
out.aRealDoorOpeningStillAnimates = onRealChange
.filter((entry) => /op-leaf/.test(entry.cls)).length >= 1;
// ---- 3) состав списка меняется ВНУТРИ пространства (#534) --------------
// Внешний ключ `keyed(space.id, …)` снимает дорогой диф на смене
// пространства, но внутри пространства состав списков всё равно ездит:
// у маркеров — призраки редактора и живой синк, у проёмов — запись с
// нерешённым хостом, которая живёт ТОЛЬКО в режиме plan. Если убрать
// внутренний `repeat`, позиционное переиспользование вернётся через эту
// дверь, и ни одна проверка выше не покраснеет.
await settle();
const byId = (nodes) => new Map(nodes.map((node) => [node.dataset.id, node]));
const markerNodes = () => [...root().querySelectorAll('[data-hp="device"]')];
const openingNodes = () => [...root().querySelectorAll('.opening')];
const markersKept = byId(markerNodes());
const template = c._renderDevices.find((device) => device.space === c._space);
out.markerTemplateFound = !!template;
c._devices = [{ ...template, id: 'hp-534-probe', name: 'probe' }, ...c._devices];
c._cfgEpoch++;
c.requestUpdate();
await c.updateComplete;
await frame();
const markersAfterInsert = byId(markerNodes());
const movedMarkers = [...markersKept].filter(([id, node]) =>
markersAfterInsert.has(id) && markersAfterInsert.get(id) !== node);
out.listGrewInsideTheSpace = markersAfterInsert.size > markersKept.size;
out.noMarkerNodeSwappedInsideTheSpace = movedMarkers.length === 0;
out.swappedMarkerNodes = movedMarkers.map(([id]) => id);
out.quietAfterTheInsert = runningAnimations().filter((entry) => !EXPECTED(entry)).length === 0;
// Проёмы: запись с нерешённым хостом отдаётся только в режиме plan, поэтому
// вход в редактор добавляет её в список и сдвигает позиции остальных.
await settle();
const space = c._serverCfg.spaces.find((item) => item.id === c._space);
const donor = space.openings[0];
out.openingDonorFound = !!donor;
space.openings = [
{ ...donor, id: 'hp-534-orphan', host: { kind: 'partition', id: 'hp-534-missing' } },
...space.openings,
];
c._cfgEpoch++;
c.requestUpdate();
await c.updateComplete;
await settle();
const openingsKept = byId(openingNodes());
c._setMode('plan');
await c.updateComplete;
await frame();
const openingsInPlan = byId(openingNodes());
const movedOpenings = [...openingsKept].filter(([id, node]) =>
openingsInPlan.has(id) && openingsInPlan.get(id) !== node);
out.orphanAppearsOnlyInPlan = openingsInPlan.size > openingsKept.size;
out.noOpeningNodeSwappedInsideTheSpace = movedOpenings.length === 0;
out.swappedOpeningNodes = movedOpenings.map(([id]) => id);
out.quietAfterTheModeSwitch = runningAnimations()
.filter((entry) => /op-leaf|op-arc/.test(entry.cls)).length === 0;
return out;
});
checkAll(res, { unexpectedAnimations: [], reusedMarkerNodes: [] });
checkAll(res, { unexpectedAnimations: [], reusedMarkerNodes: [],
swappedMarkerNodes: [], swappedOpeningNodes: [] });
await finish(browser, res);
+137 -137
View File
@@ -1,109 +1,109 @@
{
"schema": 1,
"fingerprint": "dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039",
"fingerprint": "b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 287242,
"initialViewGzipBytes": 287302,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-card.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 289548,
"initialPanelGzipBytes": 289606,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2306,
"initialPanelOnlyGzipBytes": 2304,
"lazyFiles": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/de-BAfQ3noX.js",
"houseplan-assets/editor-Ca9vnH-K.js",
"houseplan-assets/fr-a4v8Ohtu.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js",
"houseplan-assets/hp-zigbee-topology-overlay-B_T0ilCj.js",
"houseplan-assets/iso-scene-render-DtlvcvOT.js",
"houseplan-assets/live-interaction-runtime-CjYDG1SP.js",
"houseplan-assets/pdf-export-CY9TOlZ5.js",
"houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js",
"houseplan-assets/zigbee-topology-runtime-Cez46hoy.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/de-Cl_N0-YX.js",
"houseplan-assets/editor-DAm0GfXr.js",
"houseplan-assets/fr-D490TXwk.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js",
"houseplan-assets/hp-zigbee-topology-overlay-xOc6gz5D.js",
"houseplan-assets/iso-scene-render-B6PFBRuR.js",
"houseplan-assets/live-interaction-runtime-uOonwwaS.js",
"houseplan-assets/pdf-export-CYH2TEQI.js",
"houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js",
"houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js"
],
"lazyGzipBytes": 417431,
"lazyGzipBytes": 417421,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/editor-Ca9vnH-K.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"houseplan-assets/zigbee-topology-DitREaKv.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/editor-DAm0GfXr.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js"
],
"lazyEditorGzipBytes": 204999,
"lazyEditorGzipBytes": 204997,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js"
],
"lazyOnboardingGzipBytes": 13916,
"lazyOnboardingGzipBytes": 13918,
"lazyLocaleFiles": [
"houseplan-assets/de-BAfQ3noX.js",
"houseplan-assets/fr-a4v8Ohtu.js"
"houseplan-assets/de-Cl_N0-YX.js",
"houseplan-assets/fr-D490TXwk.js"
],
"lazyLocaleGzipBytes": 54820,
"lazyLocaleGzipBytes": 54821,
"lazyIsometricFiles": [
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/iso-scene-render-DtlvcvOT.js"
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/iso-scene-render-B6PFBRuR.js"
],
"lazyIsometricGzipBytes": 13460,
"lazyIsometricGzipBytes": 13456,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js"
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js"
],
"lazyFurnitureArtGzipBytes": 10254,
"lazyFurnitureArtGzipBytes": 10251,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/pdf-export-CY9TOlZ5.js"
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/pdf-export-CYH2TEQI.js"
],
"lazyPdfGzipBytes": 120128,
"lazyPdfGzipBytes": 120123,
"files": [
{
"path": "houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"sha256": "ce22e7599ea095db52bf13da52204107ed2d535fb57f69766e3669dfea1c9ef2",
"path": "houseplan-assets/backdrop-pick-BVE5pvXV.js",
"sha256": "04f4f6ebe74df3384c918202679b3ff9380729b553ff798dafe33ba5231eaaa4",
"rawBytes": 19956,
"gzipBytes": 6879,
"gzipBytes": 6880,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BAfQ3noX.js",
"sha256": "38d0ffe6551c85bc7a5686943679357a726928644136850dc646e0cdd5713bab",
"path": "houseplan-assets/de-Cl_N0-YX.js",
"sha256": "2ea60918c00765be8acb766982dcc9b58715271c62f2eef3dabc07218dd70f64",
"rawBytes": 91402,
"gzipBytes": 27694,
"gzipBytes": 27695,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Ca9vnH-K.js",
"sha256": "fd8b9731bc4a417cb0545e06677c836e98fa59a7b28e7cf0345d87c25f4aaed7",
"path": "houseplan-assets/editor-DAm0GfXr.js",
"sha256": "2524ee570c6d20502ae83cbacddc596882d293b27f7943ae59521c7b1923e796",
"rawBytes": 3832,
"gzipBytes": 1583,
"gzipBytes": 1586,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-a4v8Ohtu.js",
"sha256": "ccc6ccd7d3736f7567080f000de2dd04ecf60989163720e46d11063cd3ace9ee",
"path": "houseplan-assets/fr-D490TXwk.js",
"sha256": "8ff8d7ac1efd3359263752a7e11a26b3c34bc12ae874108c684a07ea2ff9590f",
"rawBytes": 93667,
"gzipBytes": 27126,
"isEntry": false,
@@ -111,199 +111,199 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"sha256": "d8e59b11da6c8f4a665b33cc6d92e9eb9e2e9ef94fed1071ba5988ce5c98e438",
"path": "houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"sha256": "016eabf8c967f1a22d567a35f83102a80b9d14ce864162308bb833855e440c40",
"rawBytes": 33655,
"gzipBytes": 10254,
"gzipBytes": 10251,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/guard-BYWHhzjw.js",
"sha256": "b81804f104d25d22dbc909341c48821dbe950aad94054b9512f5322a04fdfc8b",
"path": "houseplan-assets/guard-zx2aaPuf.js",
"sha256": "3455ec5c4d0ea4b31e517eb4aa029de858d0078fc6e59d49737612224440a98f",
"rawBytes": 518,
"gzipBytes": 369,
"gzipBytes": 371,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DEVy5dLN.js",
"sha256": "d8cf65be7367f51ed4c8069825965c002036ec16321e3dc83dce1638ec7d2565",
"rawBytes": 1020996,
"gzipBytes": 286437,
"path": "houseplan-assets/houseplan-card-bOJWMeFY.js",
"sha256": "69a22d0bc488c22923dcc434d2e77fa2d9f12eb75a37c4b388507de7a4d9ce9d",
"rawBytes": 1021194,
"gzipBytes": 286498,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BAfQ3noX.js",
"houseplan-assets/editor-Ca9vnH-K.js",
"houseplan-assets/fr-a4v8Ohtu.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js",
"houseplan-assets/hp-zigbee-topology-overlay-B_T0ilCj.js",
"houseplan-assets/iso-scene-render-DtlvcvOT.js",
"houseplan-assets/live-interaction-runtime-CjYDG1SP.js",
"houseplan-assets/pdf-export-CY9TOlZ5.js",
"houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js"
"houseplan-assets/de-Cl_N0-YX.js",
"houseplan-assets/editor-DAm0GfXr.js",
"houseplan-assets/fr-D490TXwk.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js",
"houseplan-assets/hp-zigbee-topology-overlay-xOc6gz5D.js",
"houseplan-assets/iso-scene-render-B6PFBRuR.js",
"houseplan-assets/live-interaction-runtime-uOonwwaS.js",
"houseplan-assets/pdf-export-CYH2TEQI.js",
"houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BVcD08Op.js",
"sha256": "b26691e5ac6338578a23ab3d6c0a774e36c4e708f2bb4a48894ab7e4b0ca7c96",
"path": "houseplan-assets/houseplan-editor-runtime-B0CHFoUQ.js",
"sha256": "7433aa76bf0dd431661b9ca16546c81599493662392375ea421940a13e6c865f",
"rawBytes": 668813,
"gzipBytes": 178484,
"gzipBytes": 178479,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-Cez46hoy.js"
"houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-0ApfaTVi.js",
"sha256": "e1fc28837937564eee4a602230cdaf3fbabe35703a7ecda43ce74def1159d0b7",
"path": "houseplan-assets/houseplan-onboarding-runtime-Dts4ebgR.js",
"sha256": "66085f37bd788c2f5e2d50d580655e93325a6b707b003500b953fdf815b926d8",
"rawBytes": 28287,
"gzipBytes": 7037,
"gzipBytes": 7038,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-yGeZ_1BL.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/backdrop-pick-BVE5pvXV.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-B_T0ilCj.js",
"sha256": "d9f0676bc0034a3baed8c9108f8799aa261cbabbef86929ef812cd90e0e9e302",
"path": "houseplan-assets/hp-zigbee-topology-overlay-xOc6gz5D.js",
"sha256": "c1007a28222b185c910284669d96f6dc941a2d7606b3194086f8c4fb29cdfd6f",
"rawBytes": 10756,
"gzipBytes": 3684,
"gzipBytes": 3683,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js",
"houseplan-assets/zigbee-topology-runtime-Cez46hoy.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js",
"houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-DtlvcvOT.js",
"sha256": "ad3e8c6b41caf6edc5e3b259dcffa3bff98c0110ae80cbd3bf736d5eccfb7d0f",
"path": "houseplan-assets/iso-scene-render-B6PFBRuR.js",
"sha256": "aa5903e34a2fd82469d195a19e4288c07b29d20130a9f99ed649903c2c6d6353",
"rawBytes": 38562,
"gzipBytes": 13091,
"gzipBytes": 13085,
"isEntry": false,
"imports": [
"houseplan-assets/guard-BYWHhzjw.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/guard-zx2aaPuf.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-CjYDG1SP.js",
"sha256": "886197ec8207c347463a743dd3a7a2f95bb05efbe4545332a41c12891cc3242e",
"path": "houseplan-assets/live-interaction-runtime-uOonwwaS.js",
"sha256": "b26b6e1cadb17533b49d7d824cccc4a649def8557953e632b8075fa2da358594",
"rawBytes": 6032,
"gzipBytes": 2433,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CY9TOlZ5.js",
"sha256": "0fa7863db13ffe8d654f35cf25222639be2f6c4ed018941feffde0503da2b425",
"path": "houseplan-assets/pdf-export-CYH2TEQI.js",
"sha256": "1bf1783f9f3f80b37d38b5000a831860334f1a4cb1aacb58dc172b6c9f25f65b",
"rawBytes": 236244,
"gzipBytes": 109874,
"gzipBytes": 109872,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-CWJaBLIz.js",
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/furniture-plan-art.generated-d7Er55Or.js",
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DeBQ1tcV.js",
"sha256": "b1bbc6089d46f90c81c295e0305374ca2f2c5fca0322f27d4e9ecb9bb76ead28",
"path": "houseplan-assets/summary-panel-editor-TASGFcvs.js",
"sha256": "9e4e2bd331c2f5fdfb6cc6719f09795c5556d712fd961e2cd18d561f2dfee7e2",
"rawBytes": 14824,
"gzipBytes": 3401,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-B3ruGJoU.js",
"sha256": "d68458815e4a7772cd87bf55f1448ba1794c54b1e6c749825f0672952051a93c",
"path": "houseplan-assets/summary-panel-metrics-bIh_Q9NB.js",
"sha256": "e50c875e83ee220fc99121b013f04f3417b8c1412d2d08265b94f4d2e3c74f57",
"rawBytes": 2610,
"gzipBytes": 1306,
"gzipBytes": 1308,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-CB4AHgRN.js",
"sha256": "d7d78a52ff91ce0dd1d07eb8e670e931814ef71c969f67069485d1313adeafe4",
"path": "houseplan-assets/summary-panel-runtime-loaded-cFcfVYdb.js",
"sha256": "78d50efb02e10eaeb8a5c4d299e98f298606d1d95a419e6bfdfdf8dc37ff36a9",
"rawBytes": 72272,
"gzipBytes": 19728,
"gzipBytes": 19727,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DeBQ1tcV.js",
"houseplan-assets/summary-panel-metrics-B3ruGJoU.js"
"houseplan-assets/summary-panel-editor-TASGFcvs.js",
"houseplan-assets/summary-panel-metrics-bIh_Q9NB.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-DitREaKv.js",
"sha256": "d29bb086e6862441a6115d431ed71e4ee0b96201a10bdfedb93d3f11280264ca",
"path": "houseplan-assets/zigbee-topology-70mzkKN3.js",
"sha256": "789d0ec02c2b0133838b379975944bb2730ee6c4679c9262000861d8457535a5",
"rawBytes": 18961,
"gzipBytes": 7430,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-Cez46hoy.js",
"sha256": "5bb047f1c003a211e0a57f2fa03d37e968144ca741d99b2f3e486ee70e4aa130",
"path": "houseplan-assets/zigbee-topology-runtime-CE0rjXi5.js",
"sha256": "04c8c7571c42d0659a412b4d560d3dbc504209631591f58ab7c86afd3580fedf",
"rawBytes": 3941,
"gzipBytes": 1765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js",
"houseplan-assets/zigbee-topology-DitREaKv.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js",
"houseplan-assets/zigbee-topology-70mzkKN3.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "2cf259caacb83ef37c3e5e743f88bcd61a6f48131af71fa565528f409b2f1f82",
"sha256": "6bf9432b3ce24211e3771939aca869d04bf03e4412c118129b3559d9561d6923",
"rawBytes": 1205,
"gzipBytes": 805,
"gzipBytes": 804,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DEVy5dLN.js"
"houseplan-assets/houseplan-card-bOJWMeFY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "02fa46808fbf68c9245ec2b518ec2047953678b13b054a923259f1a8d9e9cb9a",
"sha256": "fbdee8bf86166bcbca6e899b1ffb00f840eda83bb4a7edb66390bd08982c5a7b",
"rawBytes": 6124,
"gzipBytes": 2306,
"gzipBytes": 2304,
"isEntry": true,
"imports": [
"houseplan-card.js"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";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-DEVy5dLN.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__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";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-bOJWMeFY.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}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{dS as r,dT as t,dU as s}from"./houseplan-card-DEVy5dLN.js";const e={},a=r(class extends t{constructor(){super(...arguments),this.ot=e}render(r,t){return t()}update(r,[t,e]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return s}else if(this.ot===t)return s;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,e)}});export{a as i};
+1
View File
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{dS as r,dT as t,dU as a}from"./houseplan-card-bOJWMeFY.js";const s={},e=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 a}else if(this.ot===t)return a;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{e as i};
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
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{b as a,A as e}from"./houseplan-card-DEVy5dLN.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-CB4AHgRN.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__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{b as a,A as e}from"./houseplan-card-bOJWMeFY.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-cFcfVYdb.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">
<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"
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{e9 as e,L as t,a2 as n,J as i,an as o,N as r,bo as s,bn as a,a1 as c,a0 as l,ax as d,ea as u,eb as f,bq as m,ec as g}from"./houseplan-card-DEVy5dLN.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=d(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const r=i.binding.slice(0,o),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,c="entity"===r?e.registry.entities?.[s]:null,l="device"===r?s:c?.device_id,d="device"===r?a?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===r&&n.liveEntities.has(s))&&t.add(l)}return t}function y(e,t){return e?.length?t?.length?g(e,t):e:t}function b(e,t){const n=c(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,r);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:l(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*_(c,l,d=b){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||a;v=y(v,s(c,m.physicalBodies)),yield}const b=m.cellCm/o;u+=a(v)*b*b/1e4}return u}catch{return null}}function h(e,t,n=b){const i=_(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function p(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:m(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function S(e,t){const n=e?.states?.[t];if(!n)return null;const i=u(e,t);return i?f(i,String(n.attributes?.unit_of_measurement||"")):null}export{_ as cleanFloorAreaSteps,v as representedHaDeviceIds,b as spaceWallGeometry,S as summaryEntityValue,p as summarySystemValue,h as totalCleanFloorAreaM2};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{e9 as e,L as t,a2 as n,J as i,an as o,N as a,bo as r,bn as s,a1 as c,a0 as l,ax as d,ea as u,eb as f,bq as m,ec as g}from"./houseplan-card-bOJWMeFY.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=d(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const a=i.binding.slice(0,o),r=i.binding.slice(o+1),s="device"===a?e.registry.devices?.[r]:null,c="entity"===a?e.registry.entities?.[r]:null,l="device"===a?r:c?.device_id,d="device"===a?s?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===a&&n.liveEntities.has(r))&&t.add(l)}return t}function y(e,t){return e?.length?t?.length?g(e,t):e:t}function _(e,t){const n=c(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,a);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:l(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,a)}}function*h(c,l,d=_){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const s=t(e);if(!s)continue;const c=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,a,g.roomGeom,g.multiWallNodes)||s;v=y(v,r(c,m.physicalBodies)),yield}const _=m.cellCm/o;u+=s(v)*_*_/1e4}return u}catch{return null}}function p(e,t,n=_){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function b(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:m(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function S(e,t){const n=e?.states?.[t];if(!n)return null;const i=u(e,t);return i?f(i,String(n.attributes?.unit_of_measurement||"")):null}export{h as cleanFloorAreaSteps,v as representedHaDeviceIds,_ as spaceWallGeometry,S as summaryEntityValue,b as summarySystemValue,p as totalCleanFloorAreaM2};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";import{n as t,b as n}from"./zigbee-topology-DitREaKv.js";import{B as e}from"./houseplan-card-DEVy5dLN.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function m(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,m=null;const y=new Promise(t=>{u=t}),b=new Promise((t,n)=>{f=t,m=n}),w=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&w.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&m?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&w.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(b,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of w)try{t()}catch{}}})}export{f as readZhaTopology,m as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";import{n as t,b as n}from"./zigbee-topology-70mzkKN3.js";import{B as e}from"./houseplan-card-bOJWMeFY.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function m(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,m=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,m=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&m?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,m as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="dfcbb1b71778cc7ba07be84f07b876575d5c63600838b93cdb2db147b99ec039";try{await import("./houseplan-assets/houseplan-card-DEVy5dLN.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).":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-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 houseplan-card.js: the implementation chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b3e08d50a47faaba7562dc080f267ca13de845113735892a9115a6cdd991392a";try{await import("./houseplan-assets/houseplan-card-bOJWMeFY.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).":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-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 houseplan-card.js: the implementation chunk is unavailable",e)}
+1 -1
View File
File diff suppressed because one or more lines are too long
+17
View File
@@ -2,6 +2,23 @@
## Unreleased
- Switching between floors on a large plan is quick again. Every switch was
rebuilding the device markers the slow way — comparing two hundred old
markers against two hundred new ones only to throw all of them away — which
cost about a tenth of a second of a frozen card each time. Nothing looks
different, and the door that never moved still does not animate
([#534](https://github.com/Matysh/houseplan-card/issues/534)).
- The “Follows the sun” background no longer slows the plan down. The plan's
outer outline is a blur over the whole sheet, and it lived in the same layer
as the plan: any mouse movement made the engine re-run it. The outline now
keeps its own layer. Measured on a demo-stand pan, rasterization fell from
fifteen times a static background to the same order as one. The view is the
same: the outline around the plan matches pixel for pixel, and the hatch
inside the walls is anti-aliased a shade differently — splitting the layer
moves it by a sub-pixel
([#532](https://github.com/Matysh/houseplan-card/issues/532)).
## v1.74.0 — 2026-09-11
- The plan is noticeably smoother, and in Firefox the difference is dramatic.
+16
View File
@@ -8,6 +8,22 @@
## Не выпущено
- Переключение между этажами на большом плане снова быстрое. Каждое
переключение пересобирало маркеры устройств самым долгим способом — сверяло
двести старых маркеров с двумя сотнями новых, чтобы затем выбросить все, — и
это стоило около десятой доли секунды подвисшей карточки. Внешне не меняется
ничего, и дверь, которая никуда не двигалась, по-прежнему не анимируется
([#534](https://github.com/Matysh/houseplan-card/issues/534)).
- Фон «Следует за Солнцем» больше не замедляет план. Внешний контур плана —
это размытие по габариту всего листа, и оно жило в общем слое с планом: любое
движение мыши заставляло движок прогонять его заново. Теперь контур держится
отдельным слоем. Замер на панораме демо-стенда: растеризация упала с
пятнадцатикратной к статичному фону до сопоставимой с ним. Вид прежний:
ореол вокруг плана совпадает пиксель в пиксель, а штриховка внутри стен
сглаживается чуть иначе — разделение слоёв сдвигает её на субпиксель
([#532](https://github.com/Matysh/houseplan-card/issues/532)).
## v1.74.0 — 2026-09-11
- План стал заметно плавнее, а в Firefox разница разительная. Перетаскивание
+11
View File
@@ -75,6 +75,17 @@ The public setting remains a two-value selector. `static` uses `bg_color`.
05:00→13:00→21:00 fallback arc. It has zero visible opacity at night.
- Four constant environment layers cross-fade for exactly 1100 ms with
`cubic-bezier(.22,.61,.36,1)`. Reduced motion disables the transition.
- The zero-offset outline is a filter over the grouped paper footprint, so it
is kept on its own compositing layer (`will-change: filter`, scoped to the
day-cycle stage). The group holds paper silhouettes only and does not change
while the plan is hovered or panned; without the hint every repaint of the
plan re-ran three blur passes over the whole sheet. Measured in headless
Chromium (CDP tracing, summed `RasterTask`) on a demo-stand pan, that cost
about fifteen times the rasterization of a static background; the owner's
Firefox profile that opened #532 showed the same cause as 23 MB of texture
uploads per frame and about nine frames per second, without a comparable
ratio of its own. A static background carries neither the filter nor the
hint.
- Only the environment and the zero-offset alpha-aware outline outside the
grouped plan-paper footprint change. The plan, paper, floors, room fills,
Glow/spill, devices, labels, decor/backdrop, vacuum, hover, and window rays
+12 -12
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceFingerprint": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"captureScriptSha256": "9e4b0ae533407fe003a129078aec020f2c83d67cdb6fc6da696ce3d829beaeb5",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,7 +15,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "76c35a87810e3dc6890bda7b43da133f57d8860031204a62cda7115c15950090"
},
"view-touch": {
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "795585acb09488236ebb8028a7fcbd918404c5fe01560bb9b4559de13d441625"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "9f0970de09772674c3156b9fc5b2d49de0ceb22be11ecf8b136047d37d100a84"
},
"room-contour-close": {
@@ -48,7 +48,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "0c354a79f9c3660788b6c5e889a27b6bd78b1f209dc0eef6bb43e2c1344028c4"
},
"plan-context-tray": {
@@ -59,7 +59,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "64169c543291bc4df806dcf8ff484b42c8ea55eb343323d945344e0e28f2ce97"
},
"device-editor": {
@@ -70,7 +70,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "3c8703175605fcf0387d654cd30783ecfd33e21743e70b45190a5ae1a8d355c2"
},
"device-display-preview": {
@@ -81,7 +81,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "5d6193202102f5e6d85567f1111fa105895b933979fbf369c6ba0b1029deb823"
},
"background-editor": {
@@ -92,7 +92,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "375900a29aa62632883ede6727b7dbb8b48cabe9d7d0e16425f9779c837f3dd9"
},
"room-card": {
@@ -103,7 +103,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "3d85424676c823ddca3fa7dab3cce089b6030e48c05d16d2d48d922c66c0cbd2"
},
"device-info": {
@@ -114,7 +114,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "ac962820f8e1c5cb5b042d39b18dc95905fa840cb2d4749060d991be4f6977c2"
},
"pdf-export": {
@@ -125,7 +125,7 @@
},
"theme": "light",
"language": "en",
"sourceSha256": "45f506aca7baceaa68ed03b4a7d97f05c8c22c42976287b6f3b42274f0122327",
"sourceSha256": "d11c6433c3b10ec39a97f45ea5a1ea80da8443b80f8ab34a6e7bd4bd70aee64a",
"imageSha256": "4537692fd57a3ed95528eddd748eee5cb6e456ffab7cc49169853d62459252cd"
}
},
+207
View File
@@ -0,0 +1,207 @@
# CODE-REVIEW-532-r1
Вердикт: **жёлтый** · заход r1 · блокирующих циклов израсходовано 0 из 4 · High: 0 · Medium: 2 (в скоупе, возврат автору)
Материал: `bb04f8605f9003c0ec6643359fa47af9319247bc` (ветка приведена к `dev`
конвейером; `origin/dev..HEAD` содержит один коммит, диплист диффа полный, не по
дельте — цикл первый).
## Скоуп
Issue #532: тройной `drop-shadow` внешнего контура плана (`.hp-paperg`) живёт в
общем композиционном слое с планом, поэтому любая перерисовка плана (ховер,
панорама) заново прогоняет три прохода размытия по габариту всего листа —
на машине владельца (Firefox) это ~9 к/с и 23 МБ текстур/кадр. Правка:
`will-change: filter` на правиле `.stage.daycycle .hp-paperg,
.hp-static-stage.daycycle .hp-paperg`, уводящее отфильтрованную бумагу в свой
слой. Побочный эффект контракта (К2, принят на спек-ревью r2): промоушен делит
SVG на два слоя, из-за чего диагональная штриховка стен антиалиасится иначе —
4 golden-кадра `day-cycle-{dawn,day,dusk,night}-dark` расходятся и подлежат
пересъёмке на кандидате беты, а не в этом коммите.
Продуктовая рамка: View-режим — продукт для двух из трёх персон (`SCOPE.md`),
настенный планшет и телефон читают план именно там; ускорение растеризации
дневного цикла закрывает именно этот сценарий, палитру/геометрию/слои
окружения/блендинг не трогает (К3, К4 — вне скоупа, подтверждено кодом, см.
ниже). Продукт кода вне `plan.styles.ts` не тронут.
Спек-ревью прошло два раунда (r1, r2 — оба зелёные); r2 переписал К2/AC3 после
того, как автор сам обнаружил на реализации, что «golden останется зелёным» не
подтвердилось, и вернул ТЗ на переработку вместо подгонки факта под старый
текст. Код-ревью (этот документ) — первый заход, полный разбор.
## Как проверялось
Дешёвые гейты (`tsc`, `npm test`, `npm run build` + сверка 3 копий бандла,
`check-docs.mjs`) подтверждены зелёным прогоном Validate на этом самом SHA
(https://github.com/Matysh/houseplan-card/actions/runs/34622197915) — повторно
не гонял, кроме случаев ниже, где перегон был нужен как побочный эффект.
Гейты, которые я прогнал сам (а не поверил хендоффу):
| Гейт | Команда | Результат |
|---|---|---|
| Смок-свидетель | `node demo/smoke_daycycle_raster.mjs` (после `npm run bundle:sync`, локальный ratio 0.73 — другая машина, тот же порядок, что у автора 0.76/0.82) | OK, 8/8 |
| Мутант | `node scripts/mutation-gate.mjs --id=daycycle-outline-not-promoted` | `тест покраснел, как обязан` — поймано 1 из 1 |
| Селектор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | НЕОПРЕДЕЛЁННОСТЬ (0 символов на изменённой строке) — совпадает с заявленным риском 5 |
| Полная golden-матрица | `node demo/golden/run.mjs --mode=verify` (169 сценариев) | ровно 4 `different`: `day-cycle-{dawn,day,dusk,night}-dark`; всё остальное `passed`, включая 7 сценариев, которые в хендоффе автора были помечены как «шум песочницы» — здесь (пиновый Chromium) они все зелёные |
| Мой собственный замер среднего цвета кадра по всем 4 кадрам (не только `night-dark`, как в хендоффе) | одноразовый скрипт через `createImageBitmap`/`getImageData`, baseline vs `artifacts/golden/actual/*`, удалён после прогона | `dawn` Δ−0.001, `day` Δ−0.014, `dusk` Δ+0.047, `night` Δ+0.080 из 255 — все четыре внутри заявленного допуска «не дальше 0.1» |
| Ограничивающий прямоугольник расхождения по всем 4 кадрам | тот же одноразовый прогон | `[120‑121, 36, 867, 757‑758]` — идентичен по всем четырём кадрам и совпадает с рамкой, которую автор привёл для `night-dark`; расхождение не задевает ореол снаружи плана |
| Три копии бандла | `diff -q dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` (+ `houseplan-assets.json`) | идентичны |
Не прогонял: `pytest tests_backend` (бэкенд не задет), `npm run invariants`
(диффа геометрии/модели/`layout`/`marker.space`/`open_spans` нет — правка
чисто в CSS), performance-профили Validate (не названы в AC), полный
`npm test`/`tsc`/`build` заново как отдельный гейт (уже зелёные на этом SHA
по Validate; `npm run build`+`bundle:sync` я всё равно выполнил как побочный
эффект, чтобы поднять локальный смок/golden — красных мест не дали).
`smoke_glow*`/`smoke_discovery_filters` не гонял: слабое совпадение по имени
(«свет», «фильтр»), но К3/К4 прямо объявляют слои окружения и блендинг вне
скоупа, а код-факт (`svgScreenBlendSupported` не читает `bg_mode`) это
подтверждает — смотреть не было необходимости.
Рабочая копия оставлена чистой: все временные скрипты и `artifacts/golden/`
удалены после использования, `git status` пуст.
## AC · чем доказан · чем краснеет (проверено самостоятельно)
| # | AC | Чем доказан | Чем краснеет | Проверено |
|---|---|---|---|---|
| AC1 | Подсказка `will-change: filter` стоит только при `daynight`, статичный фон — без неё | `demo/smoke_daycycle_raster.mjs`, часть 1 | мутант `daycycle-outline-not-promoted` | сам прогнал: смок зелёный на коде, красный на мутанте |
| AC2 | Отношение растеризации на панораме ≤ 2.0 | тот же смок, часть 2 (CDP `RasterTask`) | тот же мутант (без декларации отношение ~15) | сам прогнал: 0.73 на коде, мутант красный |
| AC3 | Изменились ровно 4 кадра `day-cycle-*-dark`; расхождение — антиалиасинг штриховки, не изменение вида (ореол цел, средний цвет кадра сдвинут ≤0.1/255) | полная golden-матрица + разбор пикселей | 5-й кадр либо сдвиг среднего цвета | сам прогнал полную матрицу (169 сценариев): ровно 4 different; сам посчитал средний цвет и bbox расхождения для **всех четырёх** кадров (не только `night-dark`, как в хендоффе) — все внутри допуска, bbox идентичен по всем четырём |
| AC4 | Статичный фон не платит: ни `filter`, ни `will-change` | тот же смок | декларация без `.daycycle` | сам прогнал вместе с AC1 |
| AC5 | `docs/SUN.md` описывает промоушен как часть контракта; оба чейнджлога на месте | построчная сверка ревьюером | расхождение документа и кода | сверил построчно — обе строки чейнджлога и абзац SUN.md на месте, но см. находки ниже |
AC3 закрыт по существу мной лично, а не на веру: спек-ревью r2 прямо
отметило, что автор экстраполировал порог сдвига цвета с одного разобранного
кадра (`night-dark`) на оставшиеся три и делегировало код-ревью обязанность
проверить это «на реальном прогоне». В хендоффе автора эта проверка
по-прежнему покрывала только `night-dark` количественно (три другие только
помечены `different` в таблице). Я прогнал полную матрицу и лично посчитал
средний цвет и bbox расхождения для `dawn`/`day`/`dusk`-dark — все три ведут
себя идентично `night-dark` (тот же bbox, тот же порядок сдвига цвета), так
что риск экстраполяции снят фактом, а не доверием.
## Находки
### Medium-1 (в скоупе) — чейнджлог утверждает то, что опровергает сам контракт задачи
`docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` (новая запись в `## Unreleased`):
> The picture is unchanged, to the pixel ([#532]).
> Картинка не изменилась ни на пиксель ([#532]).
Это буквально неверно и противоречит К2/AC3 этой же задачи: 3.3–3.5 %
пикселей на каждом из четырёх кадров дневного цикла расходятся (максимальное
отклонение канала 81 из 255) — именно поэтому эталоны `day-cycle-*-dark`
пересматриваются отдельным коммитом на кандидате беты. Сообщение самого
коммита формулирует это точнее («the diagonal wall hatch anti-aliases
differently and the four day-cycle baselines are re-taken»), а публичный
чейнджлог — нет.
**Воспроизведение:** сравнить текст `docs/CHANGELOG.md` с любым из четырёх
диффов golden-матрицы (`node demo/golden/run.mjs --mode=verify`,
`day-cycle-night-dark`: 26 442 / 770 640 пикселей выше `maxChannelDelta: 10`).
**Правка:** перефразировать обе строки чейнджлога так, чтобы не утверждать
побайтовую идентичность — например, в духе текста коммита («картинка читается
так же; антиалиасинг штриховки стен смещается на несколько пикселей, эталоны
дневного цикла пересматриваются отдельно, см. #532»).
### Medium-2 (в скоупе) — `docs/SUN.md` приписывает измерение в Chromium браузеру Firefox
`docs/SUN.md`, новый абзац (AC5):
> …without the hint every repaint of the plan re-ran three blur passes over
> the whole sheet, which cost **a Firefox window** about **fifteen times**
> the rasterization of a static background (#532).
Отношение «пятнадцать раз» — это измерение мутанта/смока через CDP-трассировку
в headless **Chromium** (см. хендофф: «без правки… отношение 15.06»; тот же
порядок в моём собственном прогоне мутанта). Профиль владельца в Firefox дал
качественные цифры (23 МБ текстур/кадр, ~9 к/с, вклад drop-shadow 72–75 % от
стоимости растеризации), но никогда не давал коэффициент «×15» — сам документ
ТЗ и ревью r1/r2 явно фиксируют, что Gecko не профилировался этим свидетелем
(«Chromium — не Gecko… приговор по Gecko даёт профиль владельца»). Формулировка
SUN.md смешивает два источника числа под одной меткой, что противоречит
собственной дисциплине проекта «одно число — один источник».
**Воспроизведение:** сверить абзац SUN.md с хендоффом/комментарием аналитики —
цифра 15× нигде не связана с Firefox, только с Chromium/CDP.
**Правка:** заменить «a Firefox window» на «our Chromium/CI measurement» (или
эквивалент), не приписывая коэффициент профилю владельца.
## Что проверено и корректно
- Селектор и объявление `will-change: filter` в `src/styles/plan.styles.ts`
дословно совпадают с К1 (в т.ч. вторая ветка селектора `.hp-static-stage`);
комментарий над правилом даёт числа Chromium/CDP корректно (852 мс / 28 мс /
108 мс) — в отличие от SUN.md, здесь атрибуция источника числа верна.
- `.hp-paperg` действительно группа только бумажных силуэтов
(`src/houseplan-card.ts:11534`, `_paperShapes(space.rooms)`), не меняется
при ховере/панораме — механика из ТЗ подтверждена чтением, не только словом
аналитика.
- `scripts/mutation-gate.mjs`: патч `daycycle-outline-not-promoted` находит
ровно ту строку, которая добавлена в диффе (сверено побайтово через `grep`),
мутация подтверждена прогоном.
- `demo/golden/matrix.mjs` содержит ровно 4 сценария с `bgMode: 'daynight'` —
структурно подтверждает и «К2 живёт только под `.daycycle`», и «ровно четыре
кадра» из AC3.
- `svgScreenBlendSupported` (`src/glow-blend.ts`) — рантайм-проба, не читающая
`bg_mode` — подтверждает К4 (блендинг света вне скоупа) кодом.
- `docs/images/screenshots.json`: изменился только `sourceFingerprint`
(отпечаток `src/**`), все 11 `imageSha256` не изменились — соответствует
заявлению «`npm run docs:accept -- --identical`», документация не поехала.
- Три копии бандла (`dist/`, `custom_components/houseplan/frontend/`)
побайтово идентичны; сборка воспроизводится (`npm run build` +
`bundle:sync` на этом дереве прошли чисто).
- Коммит несёт `Issue: #532` и `User-Visible: yes`; обе строки чейнджлога — в
том же коммите, что и поведение (не в отдельном).
- Продуктовый вопрос автора («приемлемо ли платить пересъёмкой 4 эталонов за
скорость») не новый: это ровно К2/AC3, уже разобранные и принятые зелёным
спек-ревью r2 с полным раскрытием факта (byte-for-byte ореол, сдвиг
среднего цвета, план пересъёмки с `Release:`/`Baseline-Reviewed:`). Код
реализует то, что было одобрено; переоткрывать это как продуктовое решение
на этапе код-ревью нет оснований — я лишь перепроверил цифру на всех
четырёх кадрах вместо одного (см. AC3 выше).
- Риск 5 из ТЗ (`smoke-select` не находит связь) подтверждён независимо:
тот же вывод «НЕОПРЕДЕЛЁННОСТЬ» на моём прогоне, что и в хендоффе.
## Чего не проверял
- `pytest tests_backend` — диффа в `custom_components/**/*.py` нет.
- `npm run invariants` — диффа геометрии/модели (рёбра, `layout`,
`marker.space`, `open_spans`) нет, только CSS-свойство.
- Performance-профили Validate (`performance_smoke` и т.п.) — не названы в
AC, диффа в этих путях нет.
- Приговор по настоящему Firefox (профиль владельца до/после) — вне
возможностей код-ревью; ТЗ явно оставляет это отдельным шагом суждения
владельца, а не AC этой задачи.
- Полный `smoke_*` матрица (244 файла) — не прогонял целиком; `smoke-select`
честно вернул «неопределённость», просмотрел список смоков с похожими
именами (`glow*`, `discovery_filters`) и решил не гонять — К3/К4 прямо
выводят эти поверхности из скоупа, и код (`svgScreenBlendSupported`) это
подтверждает.
---
**Материал раунда:** SHA `bb04f8605f9003c0ec6643359fa47af9319247bc`,
`origin/dev..HEAD` = 1 коммит, диапазон диффа `origin/dev...HEAD` (53 файла,
включая 3 копии бандла и golden-fingerprint документации). Первый заход —
раздела «Унаследовано из r0» нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/532-daycycle-raster`, коммит `bb04f8605f90` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `959371418fa5d512b6314804fb4d9cf13e86efe4`
```
git log --all --format='%H %T' | grep 959371418fa5
```
- Тело issue: `12e66588332b94c78100156f01e4832881b8163fb6f8fdca83acfcb8e90f4545`
- Вердикт конвейера: `yellow` · High 0
+181
View File
@@ -0,0 +1,181 @@
# CODE-REVIEW-532-r2
Вердикт: **зелёный** · заход r2 · блокирующих циклов израсходовано 1 из 4 · High: 0 · Medium: 0
Материал: `1cfe7fa53c71b2ba595c92d1bdfc560f90242f40`. Ветка приведена к `dev`
конвейером: поверх головы задачи (`a89bd1b8`, тот же коммит, что автор назвал
хендоффом r2) легло 7 коммитов `dev` (релиз v1.74.0 и посторонняя задача
#533), ветка перемотана на `a89bd1b8 → 1cfe7fa5`. Это другой код по §7.2 —
разбор ниже полный, а не только по дельте, хотя `origin/dev..HEAD` содержит
всего 3 коммита самой задачи.
## Скоуп
Issue #532 (не изменился с r1): тройной `drop-shadow` внешнего контура плана
(`.hp-paperg`) жил в общем композиционном слое с планом, поэтому любая
перерисовка плана (ховер, панорама) заново прогоняла три прохода размытия по
габариту всего листа — в Firefox владельца ~9 к/с, 23 МБ текстур/кадр. Правка:
`will-change: filter` на `.stage.daycycle .hp-paperg, .hp-static-stage.daycycle
.hp-paperg`, уводящее отфильтрованную бумагу в свой слой. Побочный эффект
контракта (К2, принят спек-ревью r2): промоушен делит SVG на два слоя, отчего
диагональная штриховка стен антиалиасится иначе — 4 golden-кадра
`day-cycle-{dawn,day,dusk,night}-dark` расходятся и пересматриваются отдельным
коммитом на кандидате беты. Продуктовая рамка (View-режим как продукт для двух
персон, `docs/SCOPE.md`) не тронута этим раундом — перечитал `SCOPE.md`,
вывод не изменился.
Раунд r2 — это ровно исправление двух Medium-находок r1: оба чейнджлога и
`docs/SUN.md`. Продуктовый код (`src/styles/plan.styles.ts`), смок
(`demo/smoke_daycycle_raster.mjs`) и мутант (`scripts/mutation-gate.mjs`)
байт-в-байт не изменились с r1 — доказано ниже, не заявлено.
## Что изменилось между r1 и HEAD (доказательство байт-в-байт для остального)
```
git diff bb04f8605f9003c0ec6643359fa47af9319247bc ced9b416 -- \
src/styles/plan.styles.ts demo/smoke_daycycle_raster.mjs \
scripts/mutation-gate.mjs docs/images/screenshots.json
```
→ пустой вывод: коммит `ced9b416` (содержимое головы задачи после ребейза)
идентичен коммиту `bb04f8605f90`, который разобрало r1, по всем файлам кроме
самих чейнджлогов/`SUN.md`. Единственный новый коммит раунда —
`1cfe7fa53c71b2ba595c92d1bdfc560f90242f40` («Say what the day-cycle layer
change does to the picture...»), и он трогает только `docs/CHANGELOG.md`,
`docs/CHANGELOG.ru.md`, `docs/SUN.md` (14 строк, `User-Visible: no`).
Это не молчаливое доверие: равенство деревьев проверено мной командой выше, а
не взято со слов автора.
## Как проверялось
Validate на `1cfe7fa5` зелёный
(https://github.com/Matysh/houseplan-card/actions/runs/34625301309) —
`tsc`, `npm test`, `npm run build`+сверка бандла, `check-docs.mjs` не
перегонял отдельно, кроме случаев ниже.
Прогнал сам (диф трогает рендер CSS через `ced9b416`, хоть и унаследованный
от r1 — golden/смок/мутант не покрыты Validate):
| Гейт | Команда | Результат |
|---|---|---|
| Сборка + синхронизация 3 копий бандла | `npm run build && npm run bundle:sync` | чисто, `git status` пуст после — рабочее дерево воспроизводит закоммиченный `dist`/`custom_components/houseplan/frontend`/`demo/srv/assets` побайтово |
| Смок-свидетель | `node demo/smoke_daycycle_raster.mjs` | OK, 8/8 (`ratio: 0.78`, другая машина, тот же порядок, что у автора 0.73–0.82) |
| Мутант | `node scripts/mutation-gate.mjs --id=daycycle-outline-not-promoted` | «тест покраснел, как обязан» — поймано 1 из 1, рабочее дерево осталось чистым после |
| Три копии бандла | `diff -q dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` + `houseplan-assets.json` | идентичны |
| `check-docs.mjs` | `node scripts/check-docs.mjs` | passed (7 files, 12 external links) |
Golden-матрицу (169 сценариев) и построчный анализ 4 кадров дневного цикла
**не перегонял** — код, который её результат определяет
(`plan.styles.ts`), байт-в-байт тот же, что r1 уже прогнало полностью (ровно
4 `different`, bbox и средний цвет посчитаны для всех четырёх кадров
самостоятельно, см. `docs/reviews/CODE-REVIEW-532-r1.md`). Замена входа
исключена диффом деревьев выше — прогон на неизменных байтах дал бы тот же
результат, а сама матрица не из дешёвых.
Не прогонял: `pytest tests_backend` (бэкенд не задет), `npm run invariants`
(диффа геометрии/модели/`layout`/`marker.space`/`open_spans` нет — раунд
чисто документационный, а унаследованный код — чистый CSS), performance-
профили Validate (не названы в AC), `smoke-select` заново (диф раунда — доки,
не `src/**`; для унаследованного CSS-диффа r1 уже получило и задокументировало
«НЕОПРЕДЕЛЁННОСТЬ» и обосновало решение не гонять смежные `smoke_glow*`/
`smoke_discovery_filters` кодом К3/К4 — не переоткрываю).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| Medium-1: чейнджлоги утверждали «picture is unchanged, to the pixel» / «не изменилась ни на пиксель», что противоречит К2/AC3 (3.3–3.5 % пикселей расходятся) | Коммит `1cfe7fa5` переписал обе записи: «The view is the same: the outline around the plan matches pixel for pixel, and the hatch inside the walls is anti-aliased a shade differently — splitting the layer moves it by a sub-pixel» (и симметрично по-русски) | `docs/CHANGELOG.md:9-12`, `docs/CHANGELOG.ru.md:15-17` |
| Medium-2: `docs/SUN.md` приписывал коэффициент «×15» профилю Firefox, хотя измерен в headless Chromium через CDP | Коммит `1cfe7fa5` разделил источники: «Measured in headless Chromium (CDP tracing, summed `RasterTask`) on a demo-stand pan, that cost about fifteen times…; the owner's Firefox profile that opened #532 showed the same cause as 23 MB of texture uploads per frame and about nine frames per second, without a comparable ratio of its own» | `docs/SUN.md:82-88` |
Проверил не по заявлению автора, а построчным чтением обоих файлов после
правки (см. выше) — формулировки соответствуют находкам r1 дословно, включая
рекомендованную ревьюером r1 замену «a Firefox window» → «our
Chromium/CI measurement».
## Унаследовано из r1
Без повторного прогона (дорогие гейты), на основании доказанного выше
байт-в-байт равенства `src/styles/plan.styles.ts`, `demo/smoke_daycycle_raster.mjs`,
`scripts/mutation-gate.mjs` между `ced9b416` (голова этого раунда) и
`bb04f8605f9003c0ec6643359fa47af9319247bc` (материал r1):
- AC1/AC2/AC4 (подсказка стоит только под `.daycycle`, отношение растеризации
≤ 2.0, статичный фон не платит) — доказаны в r1 смоком + мутантом; я
независимо перепрогнал оба на текущем SHA (таблица выше) вместо чистого
наследования.
- AC3 количественно (полная golden-матрица 169 сценариев: ровно 4
`different`; средний цвет и bbox расхождения посчитаны r1 лично для всех
четырёх кадров `day-cycle-*-dark`, не только `night-dark`; семь «шумных»
кадров подтверждены контролем на базе `origin/dev` тем же Chromium) —
наследую без повторного прогона матрицы, см. документ
`docs/reviews/CODE-REVIEW-532-r1.md`, SHA `bb04f8605f9003c0ec6643359fa47af9319247bc`.
- Структурные факты кодом: `demo/golden/matrix.mjs` содержит ровно 4 сценария
с `bgMode: 'daynight'`; `svgScreenBlendSupported` (`src/glow-blend.ts`) не
читает `bg_mode` (К4 вне скоупа подтверждено кодом); `.hp-paperg` — только
бумажные силуэты (`src/houseplan-card.ts`), не меняется на ховере/панораме.
Файлы не изменились с r1 — наследую вывод.
- Риск 5 ТЗ (`smoke-select` не находит связь для чисто-CSS диффа) —
подтверждён независимо в r1, код не изменился — наследую.
- Трейлеры и три копии бандла коммита `ced9b416` (`Issue: #532`,
`User-Visible: yes`, оба чейнджлога в одном коммите с поведением) — уже
проверены в r1 и не менялись; коммит `1cfe7fa5` несёт свои корректные
трейлеры (`Issue: #532`, `User-Visible: no` — меняется только формулировка,
не поведение), проверено мной заново.
## Что проверено и корректно (этот раунд)
- Оба чейнджлога и `docs/SUN.md` больше не содержат ни одного из двух
утверждений, отмеченных r1 как противоречащих контракту задачи; новые
формулировки проверяемы читателем и не смешивают источники чисел.
- `1cfe7fa5` несёт `Issue: #532` и `User-Visible: no` — корректно: меняется
только текст о поведении, не само поведение.
- Рабочее дерево после `npm run build && npm run bundle:sync` совпадает с
закоммиченными `dist/**`, `custom_components/houseplan/frontend/**`,
`demo/srv/assets/**` побайтово (`git status` пуст) — сборка воспроизводима
на этом SHA.
- `docs/images/screenshots.json` не менялся в этом раунде (проверено диффом
дерева) — отпечаток документации не поехал.
- Продуктовый вопрос автора из хендоффа r1 («приемлема ли цена пересъёмки 4
эталонов») закрыт зелёным спек-ревью r2 и не переоткрывается: этот раунд не
трогает К2/AC3 по существу, только их описание в публичных доках.
## Чего не проверял
- `pytest tests_backend` — `custom_components/**/*.py` не задет.
- `npm run invariants` — геометрия/модель/`layout`/`marker.space`/
`open_spans` не задеты; унаследованный код — чистый CSS без геометрии.
- Полная golden-матрица заново — код, который её результат определяет, не
изменился (доказано диффом деревьев); повторный прогон на тех же байтах не
добавляет информации, а гейт дорогой.
- Performance-профили Validate — не названы в AC.
- `smoke-select` заново — диф раунда лежит только в `docs/**`, инструмент по
документации не разрешает связей; для унаследованного CSS-диффа ответ r1
(«НЕОПРЕДЕЛЁННОСТЬ») не переоткрываю.
- Приговор по настоящему Firefox (профиль владельца до/после этой конкретной
правки текста) — вне возможностей код-ревью и не AC этой задачи.
---
**Материал раунда:** SHA `1cfe7fa53c71b2ba595c92d1bdfc560f90242f40`,
`origin/dev..HEAD` = 3 коммита (`ced9b416` — голова задачи после ребейза,
байт-в-байт равна материалу r1; `3322c20c` — публикация документа r1;
`1cfe7fa5` — исправление по r1, единственный содержательный коммит раунда).
Полный разбор по указанию конвейера (ребейз через 7 чужих коммитов, §7.2), с
опорой на доказанное byte-for-byte наследование дорогих гейтов из r1.
**Предыдущий раунд:** `docs/reviews/CODE-REVIEW-532-r1.md`, вердикт жёлтый,
материал `bb04f8605f9003c0ec6643359fa47af9319247bc`.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/532-daycycle-raster`, коммит `1cfe7fa53c71` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `0da2d90f30f9e607782d065fe23fc1e770c7e676`
```
git log --all --format='%H %T' | grep 0da2d90f30f9
```
- Тело issue: `12e66588332b94c78100156f01e4832881b8163fb6f8fdca83acfcb8e90f4545`
- Вердикт конвейера: `green` · High 0
+193
View File
@@ -0,0 +1,193 @@
# CODE-REVIEW-534-r1
Issue: [#534](https://github.com/Matysh/houseplan-card/issues/534) · Материал: `5afdb644c78d60cdffa8f67a465b10af0d769c1e` (единственный коммит поверх `origin/dev`) · Заход r1 (первый код-ревью, ТЗ прошло r1→r3, зелёный вердикт на r3) · блокирующих циклов израсходовано 0/4
## Скоуп
Регресс `longTask.countP95` в линейке v1.74.0 (два профиля Full Performance:
`large-house`, `plan-snap`), бисектом сведённый к #525: списки маркеров
устройств и проёмов перевели с `map()` на `repeat(items, (o) => o.id, …)`,
чтобы Lit не переиспользовал узлы по позиции при смене пространства. На плане
с двумястами маркерами `repeat` на смене пространства честно строит две карты
непересекающихся ключей, чтобы всё равно выбросить всё и создать заново — 80 мс
на цикл, 6–13 длинных задач.
Правка: оба списка обёрнуты в `keyed(space.id, repeat(items, (item) => item.id,
…))`. Внешний `keyed` выбрасывает поддерево целиком на смене пространства
(дорогой диф не выполняется вовсе), внутренний `repeat` остаётся и защищает от
позиционного переиспользования **внутри** пространства — состав обоих списков
там тоже едет: у маркеров от призраков редактора устройств и живого синка
конфига, у проёмов от orphan-записи, которая существует только в режиме Plan.
ТЗ прошло три раунда ревью ТЗ (r1/r2 — жёлтый, обе Medium-находки о том, что
допущение о безопасности голого `map()` внутри пространства неверно — сначала
для маркеров, затем для проёмов; r3 — зелёный). Код-ревью разбирает
реализацию против финального текста ТЗ (тела issue) целиком, это первый заход
код-ревью.
## Как проверялось
Дешёвые гейты подтверждены зелёным Validate на этом SHA
([прогон 34639320867](https://github.com/Matysh/houseplan-card/actions/runs/34639320867),
`workflow_dispatch`, событие проверено через `gh run view --json jobs`):
job «Фронтенд: типы, юниты, мутанты, синхрон бандла» — success, job
«Предполётные проверки» (docs/provenance/process-gate) — success, шесть
шардов «Мутанты по диффу» — success. `smoke`/`golden`/`backend`/
`performance_smoke` в этом прогоне **skipped** — классификатор не счёл его
heavy (обычный push, не кандидат и не `full=true`), поэтому эти гейты я
прогнал сам.
| Гейт | Команда | Результат |
|---|---|---|
| Пересборка бандла | `npm run build && npm run bundle:sync` | пересобранный бандл побайтово совпал с закоммиченным (`git status` пуст после пересборки) — сверх доверия к Validate |
| `check-docs` (`src/**` тронут) | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 12 external links)` |
| Выбор смоков по диффу | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 8 прямых совпадений (все на `_openingsR`) + 1 зарегистрированная связь (`smoke_space_switch_transitions.mjs` ← `_renderDevice`); широких/неопределённых связей нет |
| Смок AC1/AC2/AC2а | `node demo/smoke_space_switch_transitions.mjs` | зелёный, все поля `true`, `unexpectedAnimations: []`, `swappedMarkerNodes: []`, `swappedOpeningNodes: []` |
| 8 прямых смоков | `node demo/smoke_{glow,isometric_live_touch,open_passage,opening_binding,opening_entity_search,opening_preview,partition_openings,registryless_opening}.mjs` | все 8 зелёные |
| Мутант AC2а (маркеры) | `node scripts/mutation-gate.mjs --id=device-markers-rendered-without-keys` | «покраснел, как обязан», 1 из 1 |
| Мутант AC2а (проёмы) | `node scripts/mutation-gate.mjs --id=openings-rendered-without-keys` | «покраснел, как обязан», 1 из 1 |
| Свидетель отклонения (см. находку/не-находку ниже) | ручная мутация: снят внешний `keyed(space.id, …)` в обоих местах, пересборка, `node demo/smoke_space_switch_transitions.mjs` | смок остаётся зелёным — подтверждает заявление автора, что внешний ключ не ловится ни одним функциональным свидетелем; дерево возвращено к исходному состоянию, `git status` чист |
| AC6 (golden, диф трогает рендер) | `npm run golden:verify` | код возврата 0, все просмотренные сценарии `passed`, включая `day-cycle-*-dark` — расхождение, которое автор описал как окружение песочницы, в этой песочнице не воспроизвелось |
| AC3/AC4 (перф) | не прогонял | см. «Чего не проверял» |
| `npm test`, `npx tsc --noEmit`, `python -m pytest tests_backend` | не прогонял отдельно | покрыты зелёным Validate на этом SHA; бэкенд не тронут (diff не касается `custom_components/**/*.py`) |
| Инварианты модели | не прогонял | diff не трогает геометрию/`layout`/`marker.space`/`open_spans` — только директиву рендера поверх тех же данных |
## Находки
Нет находок High или Medium.
**Одно отклонение от буквы ТЗ, заявленное автором открыто и проверенное мной
как корректное, не находка.** Хендофф-комментарий сообщает: мутанты на внешний
`keyed(space.id, …)` (`device-layer-loses-its-space-key`,
`opening-layer-loses-its-space-key`) написаны, прогнаны и не ловятся ни одним
тестом («0 из 1»), поэтому в реестр не добавлены. Я воспроизвёл это
независимо (см. таблицу выше): сняв внешний `keyed` в обоих местах и
пересобрав бандл, `smoke_space_switch_transitions.mjs` остаётся полностью
зелёным — идентичность узлов после смены пространства одинакова что с
внешним ключом, что без него, потому что идентификаторы маркеров и проёмов
между пространствами и так не пересекаются (это гарантия #525, не этой
задачи). Финальный текст AC5 в теле issue формулирует свидетеля внешнего
ключа не как обязательный мутант, а как «красит свидетеля AC2 **либо**
возвращает замер AC3 к 936 мс» — то есть ТЗ само называет перф-замер законной
альтернативой мутационному гейту для этой конкретной защиты, и именно замер
(бисект автора: 936,0 на `main` → 903,7/905,9 с `keyed`) и есть предъявленное
доказательство. Решение принимаю: реестр мутантов не нуждается в
заведомо неубиваемом мутанте — держать его там было бы хуже, чем не держать
(врёт о покрытии), а сам факт «не ловится ничем, кроме числа» проверен, а не
предположен.
Low, не блокирует, правки не требует: в `docs/CHANGELOG.md`/`.ru.md`
экономия описана как «about a tenth of a second» / «около десятой доли
секунды», хотя измеренная разница — около 80 мс (889,1 против 936,0 на
`main`). Это качественное округление в пользовательском тексте, а не число с
претензией на точность рядом с другим таким же числом (правило «одно
число — один источник» здесь не задето: в UI это значение нигде не
показывается вторично), так что не завожу как отдельную правку.
## Что проверено и корректно
- **К1/К2 (форма).** `src/houseplan-card.ts`: оба места — список маркеров
(было `repeat(devs, …)`) и список проёмов (было `repeat(items, …)`) — теперь
`keyed(space.id, repeat(…, (x) => x.id, …))`. Один способ на оба списка, без
исключений, как требует К2.
- **К1 (узел не переживает смену пространства).** Подтверждено исполнением:
`smoke_space_switch_transitions.mjs`, разделы 1–2, зелёные без изменений в
самом тесте (AC1 не требовал правок и остался таким).
- **К3 (пер-элементный ключ обязателен внутри пространства, для обоих
списков).** Подтверждено и чтением, и исполнением:
- `src/styles/devices.styles.ts:213` — `.device-shell-frame` действительно
хранит `transition: border-color .15s, opacity .2s` (правильно: #524 снял
только `box-shadow`, что и было находкой r1 ревью ТЗ);
- `src/houseplan-card.ts` — `_openingsR` отдаёт запись с нерешённым хостом
только когда `this._mode === 'plan'`, то есть переключение режима внутри
пространства меняет состав списка проёмов (находка r2 ревью ТЗ, принятая и
закрытая в r3);
- оба триггера воспроизведены нагрузочно новым разделом 3 смока (AC2а):
вставка устройства в начало списка и переключение Plan↔View с
orphan-записью — оба сценария не переставляют существующие узлы и не
запускают анимаций.
- **Комментарий-ловушка на месте у обеих подсистем.** `plan.styles.ts:539-554`
объясняет обе половины формы и оба триггера; `devices.styles.ts:211-213`
ссылается на него рядом с живыми переходами — ровно как требует К3
(«объясняется комментарием там же, где стоят анимации»).
- **AC5, пер-элементная половина.** Оба существующих мутанта
(`device-markers-rendered-without-keys`, `openings-rendered-without-keys`)
переанкерены под новую форму (`scripts/mutation-gate.mjs:7434-7460`) и оба
красные при снятии внутреннего ключа — проверено запуском, не заявлением.
- **AC7.** Оба чейнджлога отредактированы в том же коммите, что и поведение;
трейлеры `Issue: #534` и `User-Visible: yes` на месте; ветка
`issue/534-space-switch-keyed-layer`. Комментарий К3 стоит рядом с
анимациями (см. выше) — отдельного требования из АС7 выполнено.
- **Бюджет строк.** `test/core-file-budget.test.mjs` поднимает потолок
`src/houseplan-card.ts` на 1 (13650→13651) с датированной запиской
«`import { keyed }`, переносить нечего» — соответствует диффу: единственная
добавленная непустая строка кода вне самого рендера — это импорт.
- **Бандл.** `docs/images/screenshots.json` меняет только
`sourceFingerprint`/`sourceSha256` (пиксели те же, `imageSha256` не
тронуты) — путь `docs:accept -- --identical`, как и предписывает §8 для
правок без изменения кадра.
- **Не найдено смежных регрессий.** 8 смоков, прямо завязанных на
`_openingsR` по выбору `smoke-select.mjs`, зелёные без исключений; `npm run
golden:verify` — 0 расхождений по всей матрице в этой песочнице.
- **Скоуп.** Правка восстанавливает перф, которая была до #525, ничего не
добавляет и не убирает в контракте #525 (дверь, которой не было, по-прежнему
не анимируется) — новых экранов, полей или UX-контрактов нет; под
`docs/SCOPE.md` подпадает как поддержание существующей отзывчивости плана
(J1/J6), а не новая функциональность, так что вопрос «какую строку Core user
jobs это закрывает» решается тривиально в пользу уже закрытых.
## Чего не проверял
- **AC3 (`npm run benchmark:large-house`) и AC4 (Full Performance, 9
профилей).** Не прогонял: перф-числа в этой песочнице не воспроизводят
условия раннера (другое железо, шум), а само ТЗ называет их «ориентиром»
и явно передаёт «окончательный приговор» гейту Full Performance на
релизном раннере — это пре-релизный гейт по §8 PROCESS.md, не гейт
ревью, и для этой линейки задачи он и есть источник истины (сама задача
#534 началась с его красного прогона). Числа автора (`switchCycleMs`
889,1 против 936,0 на `main` и 871,2 до #525, «2–3» длинных задачи вместо
«7–13») внутренне согласованы с ходом бисекта в аналитике и не
противоречат ничему, что я проверил чтением или исполнением.
- **`python -m pytest tests_backend`.** Diff не касается
`custom_components/**/*.py` — гейт неприменим.
- **`node scripts/model-invariants.mjs`.** Diff не трогает геометрию, `layout`,
`marker.space` или `open_spans` — только директиву рендера поверх тех же
данных, гейт неприменим.
- **Полная матрица `demo/smoke_*.mjs` (244 файла).** Прогнаны только
выбранные диффом (8 прямых + 1 зарегистрированная связь) — задача не задевает
всё дерево смоков, полный прогон это пре-релизная обязанность.
- **Мутанты на внешний `keyed(space.id, …)`.** Не добавлял в реестр — см.
раздел «Находки»: воспроизвёл заявление автора, что такой мутант не
ловится ничем, кроме перф-замера, и посчитал реестр без него правильным
решением, а не пробелом.
## Материал раунда
- SHA материала: `5afdb644c78d60cdffa8f67a465b10af0d769c1e` (единственный
коммит `issue/534-space-switch-keyed-layer` поверх `origin/dev`).
- Диапазон: `git diff origin/dev...HEAD` (55 файлов: 3 файла класса A/B по
содержанию — `src/houseplan-card.ts`, `src/styles/plan.styles.ts`,
`src/styles/devices.styles.ts` — плюс `scripts/mutation-gate.mjs`,
`demo/smoke_space_switch_transitions.mjs`, `test/core-file-budget.test.mjs`,
оба чейнджлога, `docs/images/screenshots.json` и три копии бандла класса D).
- Рабочая копия после всех локальных прогонов и ручной проверки отклонения
возвращена в состояние коммита (`git status` пуст на момент публикации
вердикта).
## Вердикт
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/534-space-switch-keyed-layer`, коммит `5afdb644c78d` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `10993a92cf52d54a6fe21138a74f514f535f2bef`
```
git log --all --format='%H %T' | grep 10993a92cf52
```
- Тело issue: `9d969d777f9787e5a50d02b2e675ad1448e945a27b47899d2d37c445a612f30e`
- Вердикт конвейера: `green` · High 0
+246
View File
@@ -0,0 +1,246 @@
# SPEC-REVIEW-534-r1
## Скоуп
Issue #534 (`bug`, `P1`, полный трек — критерий §5 «нет влияния на
производительность» явно назван нарушенным в самой ТЗ). ТЗ живёт в теле
issue под заголовком `## ТЗ` (решение владельца #517). Issue открыт, метка
`S4-spec-review`, заход r1, бюджет циклов 0/4 не тронут.
Предмет: линейка v1.74.0 добавила 7 длинных задач P95 на профилях
`large-house` и `plan-snap` — регресс внесён #525 (перевод списка маркеров
устройств на `repeat(devs, (d) => d.id, …)`, чтобы Lit не переиспользовал
узел позиционно и не проигрывал переход на неслучившемся событии). На 200
маркерах `repeat` строит и обходит карту ключей на каждый рендер, а при
смене пространства ключи не пересекаются вовсе — диф оплачивается напрасно.
Решение ТЗ: обернуть слой маркеров в `keyed(space.id, …)`
(`lit/directives/keyed.js`) вокруг обычного `map()`, оставить проёмы на
`repeat` (там элементов на порядок меньше).
## Как проверялось
Прочитаны `docs/SCOPE.md`, `PROCESS.md` (§1–§10, включая §7.1, §2.4, §2.10,
§5, §8), тело issue #534 целиком и единственный комментарий (аналитика
Codex, S2, с бисектом и измерением на пробе). Дополнительно, раз ТЗ
опирается на конкретные строки, числа и поведение существующих механизмов,
прочитан репозиторий на материале ревью (SHA `41ae5e99`, см. блок якорей):
- `src/houseplan-card.ts:11787` (`repeat(devs, (d) => d.id, …)`, слой
маркеров) и `:12996` (`repeat(items, (o) => o.id, …)`, слой проёмов) —
оба подтверждены чтением, ссылки в ТЗ точны;
- `node_modules/lit/directives/keyed.js` — директива существует в составе
уже установленной зависимости `lit`, ссылка К2 на неё не голословна;
- `scripts/mutation-gate.mjs:7440` (`openings-rendered-without-keys`) и
`:7446` (`device-markers-rendered-without-keys`) — оба существующих
мутанта нацелены ровно на строки, которые правка меняет; find/replace
`device-markers-rendered-without-keys` перестанет применяться после
правки (текст `${repeat(devs, …)}` исчезнет) — ТЗ (AC5) корректно
предвидит необходимость его переанкеровки;
- `demo/smoke_space_switch_transitions.mjs` — прочитан целиком. Подтверждено:
AC1/AC2 действительно можно закрыть существующим смоком без правок —
проверка `noMarkerNodeReusedForAnotherDevice` (строки 124–130) судит по
идентичности узла (`entry.node.isConnected && entry.node.dataset.id !==
entry.id`), а не по побочному признаку, как и требует AC2;
- `src/styles/plan.styles.ts:531–542` — комментарий-ловушка #525
существует, называет `.device-shell-frame` (`box-shadow`) как причину
для маркеров; ТЗ в разделе «Затронутые файлы» верно указывает, что этот
комментарий нужно дополнить (К3);
- `src/styles/devices.styles.ts:196-217` — проверена природа переходов на
маркере (детали см. в находке ниже);
- `demo/fixtures/large-house.mjs:10-11` — `DEVICE_COUNT = 200`,
`OPENING_COUNT = 100` (на 3 этажа, т.е. ~33 на пространство) — цифры ТЗ о
200 маркерах точны; слово «единицы» про проёмы (К3) — преувеличение
(реально несколько десятков на этаж), но не влияет ни на один AC;
- `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` — записи #524/#525 в разделе
«Unreleased»/«Не выпущено» существуют, формат черновика (AC7) совпадает
со стилем файла;
- `test/device-marker-polish-contract.test.mjs`, `test/space-order.test.mjs`,
`test/device-face.test.mjs` — ни один не проверяет позиционную
стабильность маркера при изменении состава списка внутри одного
пространства (относится к находке ниже).
Гейты не гонялись: на этапе ревью ТЗ продуктовый код не менялся, дешёвые
гейты (`typecheck`/`test`/`build`) к этому этапу неприменимы (§2.4, §8).
## Продуктовая рамка
Сценарий и «что человек увидит» сформулированы одной фразой без терминов
реализации, как требует §7.1: «каждое переключение дороже примерно на
80 мс главного потока… карточка перестаёт отвечать на ввод чуть дольше,
чем должна» → «переключение возвращается к цене, которая была до #525».
Персона — домочадцы/гости на большом плане, `docs/SCOPE.md` называет View
mode продуктом для этих персон; регресс блокирует именно это. Полный трек
обоснован явно названным нарушенным критерием §5 («нет влияния на
производительность»), а не молчаливым выбором — соответствует требованию
2026-08-27. i18n/миграция/бэкенд и touch закрыты явным «ничего» /
«тот же путь» — ТЗ учло Low-находку из SPEC-REVIEW-525-r1 (там эти пункты
пришлось поднимать отдельно), здесь они на месте с самого начала.
## Разбор AC
- **AC1** — «остаётся зелёным без правок»: подтверждено чтением, существующий
смок уже проверяет то, что требуется, и правка К2/К3 его не задевает
структурно (тест не завязан на `repeat` изнутри, только на видимое
поведение DOM/CSS).
- **AC2** — свидетель по идентичности узла уже существует (см. «Как
проверялось»), формулировка ТЗ точно описывает то, что тест делает.
- **AC3** — численный ориентир (910 мс) подтверждён измерением автора на
`main` + патч (903,7 мс), не выдуман.
- **AC4** — внешний гейт (Full Performance), критерий однозначен (девять
профилей, включая оба проблемных).
- **AC5** — корректно предвидит необходимость правки мутанта
(подтверждено выше — старый find/replace перестанет матчиться).
- **AC6/AC7** — golden и ревью кода, стандартные, без двусмысленности.
Двусмысленности, которая заставила бы гадать «что считается прохождением»,
в текстах AC не нашёл.
## Находки
### Medium — ключевое техническое допущение К2/«Принято предположительно» №2 неверно, и это открывает непроверенный регресс той же природы, что чинит вся задача
**Утверждение ТЗ:** «Внутри одного пространства позиционное переиспользование
маркеров безопасно: после #524 на маркере не осталось переходов, которые
могла бы запустить смена значений.»
**Проверено чтением и опровергнуто:** `src/styles/devices.styles.ts:213`
несёт `.device-shell-frame { … transition: border-color .15s, opacity .2s; }`
— оба перехода живы. #524 убрал только переход `box-shadow` (см. комментарий
на той же строке: «no box-shadow here: cqw-sized, restarts on every
container resize (#524)»), а не «переходы» вообще — допущение спутало
конкретный фикс с общим случаем. `--device-shell-stroke` (цвет рамки)
переопределяется по состоянию устройства (`devices.styles.ts:296,312,322,421`
— alert-цвета) — то есть у двух РАЗНЫХ устройств в одном списке этот цвет
типично отличается.
**Почему это ломает контракт К2.** К2 предписывает конкретную реализацию:
`keyed(space.id, …)` вокруг **обычного `map()`** для слоя маркеров — то есть
внутри одного пространства элементы списка теряют пер-элементный ключ,
который сейчас даёт `repeat(devs, (d) => d.id, …)`. Если состав `devs`
меняется без смены `space.id` — а он меняется: `showGhosts` (`houseplan-
card.ts:11307`, `this._mode === 'devices' && this._showAll`) переключает
видимость части устройств в редакторе устройств, а само появление нового
устройства в пространстве от лайв-синка конфигурации (J6, «new-device
flag») тоже меняет состав `devs` без переключения пространства — Lit
переиспользует DOM-узлы **по позиции**, не по устройству. Маркер, стоявший
на позиции N, может получить данные другого устройства с другим
`--device-shell-stroke`/`opacity`, и уже живой `transition: border-color
.15s, opacity .2s` эти узла честно проиграет — ровно тот класс дефекта
(«анимация события, которого не было»), который и #525, и эта самая задача
называют главным риском (Риск 1 в ТЗ), только для другого триггера: не
переключение пространства, а изменение состава списка внутри пространства.
**Почему это не поймано ни одним AC.** `demo/smoke_space_switch_transitions.mjs`
(AC1/AC2) тестирует только переключение между двумя пространствами; ни один
существующий тест (`test/device-marker-polish-contract.test.mjs`,
`test/space-order.test.mjs`, `test/device-face.test.mjs`) не проверяет
переход, вызванный изменением состава списка маркеров внутри одного
пространства. Риски ТЗ (1–3) эту ситуацию не называют.
**Почему это в скоупе.** К2 — это ровно тот код, который правит задача;
регрессия, если она проявится, будет внесена этим же коммитом, а не
существует независимо от него сегодня (сейчас защиту даёт per-item ключ
`repeat`, который К2 явно убирает).
**Что нужно для DoR (любое из трёх, решает автор):**
1. исправить допущение и сохранить пер-элементную защиту внутри
пространства — например, `keyed(space.id, () => repeat(devs, (d) =>
d.id, …))`: смена `space.id` по-прежнему выбрасывает поддерево целиком
без дорогого сравнения с 200 чужими ключами (устраняет весь измеренный
регресс), а `repeat` внутри защищает от переупорядочивания в пределах
одного пространства, как и сегодня; либо
2. показать конкретным измерением/рассуждением, что состав `devs` в
реальных сценариях (не только в фикстуре бенчмарка) не меняется без
смены `space.id` так, чтобы порядок сдвигался — и явно принять остаточный
риск в разделе «Риски», а не в «Принятые предположения» как решённый
факт; либо
3. добавить AC и свидетеля на сценарий «состав списка маркеров меняется
внутри пространства» по образцу уже существующего свидетеля #525/#528.
**Решение ревьюера:** блокирует переход в `S5-ready` без правки одним из
трёх способов выше — Medium в скоупе задачи, чинится в этом же issue
(§2.4, §2.7 #202), отдельный issue не заводится.
### Low — «их единицы» (К3) неточно описывает число проёмов
К3 обосновывает, почему проёмы остаются на `repeat`: «их единицы, диф ничего
не стоит». По фикстуре `large-house` (`DEVICE_COUNT=200`,
`OPENING_COUNT=100` на 3 этажа) на пространство приходится порядка
30 проёмов — не «единицы», хотя и на порядок меньше 200 маркеров, так что
общий вывод (репит на проёмах дешевле) не меняется и ни один AC от этого не
зависит.
**Решение ревьюера:** не блокирует, снимается этой записью; можно поправить
формулировку заодно с остальной правкой К2, отдельного цикла не открываю.
## Что проверено и корректно
- Обязательные продуктовые разделы §7.1 (сценарий, что человек увидит до и
после) присутствуют и однозначны.
- Полный трек обоснован явно названным нарушенным критерием §5.
- i18n, миграция/бэкенд, touch — явные «нет»/«тот же путь», а не молчание
(что в #525 пришлось поднимать Low-находкой — здесь уже учтено).
- AC1–AC7 пронумерованы, у каждого указан способ доказательства; численные
пороги AC3 (910 мс) и AC4 (девять профилей) проверяемы и не выдуманы —
число AC3 сверено с измерением автора на пробе.
- AC5 корректно предвидит поломку существующего мутанта новой формой кода
и требует его переанкеровки — не тихое исчезновение защиты.
- Технические ссылки (номера строк, наличие директивы `keyed` в
зависимостях, наличие мутантов, содержимое свидетеля) подтверждены
чтением репозитория, ни одна не оказалась пустой.
- Откат — один revert, без миграции данных, корректно.
- Риски 1 и 3 предметны и привязаны к AC/гейтам, которые их ловят.
- Release-артефакты (changelog RU+EN, `User-Visible: yes`) названы верно.
## Чего не проверял
- Не запускал никакого кода и ни одного гейта — на этапе ревью ТЗ
продуктовый код не менялся, дешёвые гейты (`typecheck`/`test`/`build`)
неприменимы к этой стадии (§2.4, §8).
- Не проверял golden-эталоны и реальные тайминги на Chromium/раннере —
появятся только с реализацией; численные ориентиры ТЗ (903,7 мс, 910 мс)
доверены измерению автора (внутренне согласованы с бисектом в
комментарии) и будут окончательно подтверждены Full Performance по AC4.
- Не проверял частоту реальных сценариев `showGhosts`/лайв-добавления
устройства «в проде» количественно — риск в находке выше обоснован кодом
(условие фильтра, наличие живых CSS-переходов, наличие state-зависимого
цвета рамки), а не измерением частоты; для итогового решения (пункт 1–3
находки) этого достаточно, эмпирическая частота не меняет того, что
сценарий кодом не исключён и не покрыт тестом.
## Вердикт
Жёлтый. Один Medium в скоупе задачи (допущение К2 о безопасности
внутрипространственного позиционного переиспользования маркеров неверно и
открывает непроверенный регресс той же природы, которую чинит вся задача);
High нет. Возврат автору на правку одним из трёх названных способов;
одна Low-находка о формулировке снята этой записью.
---
<!-- material-anchors: подготовлено вручную ревьюером, конвейер §416 не запускался в этом окружении -->
## Материал раунда
- Ветка: `dev`, коммит `41ae5e99868db27e3bfde9a40426c0a636c374bd`.
- Дерево материала: `e87f2fb0f5a99184cd9795a5e59155b96b3ac2f6`
```
git log --all --format='%H %T' | grep e87f2fb0f5a9
```
- Тело issue (нормализовано `gh issue view 534 --json body -q '.body'`):
`sha256:98e12a0f7bec0c050802b1caee9c139db3bbc7b7c1169744fb1ccd567e35babd`
- Вердикт: `yellow` · High 0 · Medium 1 (в скоупе)
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `41ae5e99868d` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `e87f2fb0f5a99184cd9795a5e59155b96b3ac2f6`
```
git log --all --format='%H %T' | grep e87f2fb0f5a9
```
- Тело issue: `3ba57688eafe3310c9e3d0157758ea519c3b89d980958e504d2c2b54b3a43cb3`
- Вердикт конвейера: `yellow` · High 0
+259
View File
@@ -0,0 +1,259 @@
# SPEC-REVIEW-534-r2
## Скоуп
Issue #534, этап `spec` (ревью ТЗ, PROCESS.md §2.4), заход r2, до этого
раунда израсходован 1/4 блокирующих цикла (r1 — жёлтый, Medium в скоупе).
ТЗ живёт в теле issue (решение владельца #517).
Материал этапа spec — тело issue, не код. Между r1 и r2 продуктовый код не
менялся: `git diff 41ae5e99..HEAD -- . ':!docs/reviews'` пуст, рабочая
копия на `e40d3f18` содержит только коммит с документом
`SPEC-REVIEW-534-r1.md`. Единственная дельта раунда — правка тела issue
владельцем (комментарий-вердикт r1 и последующий комментарий Matysh от
2026-09-11T18:58:53Z). Хэш нормализованного тела изменился
(`sha256:18961b89…` в r2 против `sha256:98e12a0f…`, зафиксированного в
материале r1) — дельта подтверждена, а не предполагается.
## Как проверялось
1. Прочитан вердикт r1 (комментарий `IC_…`, 2026-09-11T18:53:19Z) и
документ `docs/reviews/SPEC-REVIEW-534-r1.md` на коммите `41ae5e99`
(дерево материала `e87f2fb0f5a9…`, зафиксировано в блоке «Материал
раунда» того документа).
2. Дельта тела issue объявлена (см. «Скоуп») и разобрана построчно:
старые формулировки К2/К3/«Принятые предположения»/AC5/«Риски»
реконструированы по цитатам r1-документа и сопоставлены с текущим
текстом (`gh issue view 534 --json body -q '.body'`, сохранено в
`/tmp/current_body.md`).
3. Технические ссылки новой редакции проверены чтением репозитория на
материале ревью (SHA `e40d3f18`, код не менялся с r1):
- `src/houseplan-card.ts:11787` (`repeat(devs, (d) => d.id, …)`) и
`:12996` (`repeat(items, (o) => o.id, …)`) — оба списка сегодня без
`keyed`, ссылки К2 «оба списка… `keyed(…, repeat(…))`» описывают
код, которого ещё нет, корректно как целевое состояние;
- `scripts/mutation-gate.mjs:7434-7456` — оба существующих мутанта
(`openings-rendered-without-keys`, `device-markers-rendered-without-keys`)
сделаны точным `find`/`replace` по буквальной строке с `repeat(…)`;
`applyPatches` (`mutation-gate.mjs:8910-8916`) бросает исключение,
если якорь не найден ровно один раз — промах анкера не тихий, гейт
красится сборкой мутанта, а не пропускает проверку молча;
- `src/styles/plan.styles.ts:531-549` — комментарий-ловушка #525 уже
сегодня объясняет `.op-leaf { transition: transform 0.6s ease }` и
`.op-arc { transition: stroke-dashoffset 0.6s ease }` как причину,
по которой список проёмов **уже с #525** рендерится через `repeat`,
наравне с маркерами устройств — тем же абзацем, который К2/К3 этого
раунда обязывают дополнить;
- `src/houseplan-card.ts:8226-8260` (`_openingsR`) — при
`!resolution.resolved` в режиме `plan` добавляется orphan-запись
(`:8242-8243`), а вне `plan` эта запись выпадает из списка
(`:8244`, `flatMap` возвращает `[]`) — состав `items` меняется от
переключения режима Plan/View **без смены пространства**, длина
массива и позиции последующих элементов сдвигаются;
- `src/houseplan-card.ts:12987-12996` (`_renderOpenings`) — рендерит
`items` из `_openingsR` тем же `repeat`, что и обсуждается в К2/К3.
Гейты не гонялись: этап spec, продуктовый код не менялся (§2.4, §8) —
как и в r1.
## Закрытие раунда r1
| находка r1 | чем закрыта | где это видно |
|---|---|---|
| Medium — допущение К2 «внутри пространства позиционное переиспользование маркеров безопасно» опровергнуто чтением (`devices.styles.ts:213`, живые переходы) | Формулировка К2 переписана: оба списка теперь `keyed(space.id, repeat(items, id, …))` — внутренний `repeat` **сохраняется**, а не убирается. Добавлен явный контракт К3 «почему пер-элементный ключ обязан остаться» с тем же техническим обоснованием, которое привёл ревьюер (переходы `.device-shell-frame`, `--device-shell-stroke` по состоянию). Допущение №2 переписано из утверждения о безопасности в утверждение о цене (диф `repeat` идёт по быстрому пути, измерено 905,9 vs 903,7 мс) — корректность больше не зависит от истинности этого допущения, потому что `repeat` защищает по ключу независимо от того, совпадают ли ключи по порядку. Добавлен новый AC2а — свидетель именно на сценарий «состав списка маркеров меняется без смены пространства». Риск 2 переписан под новый сценарий и ссылается на К3/AC2а/мутант. | Тело issue: К2 (строка 48), К3 (50), AC2а (60), Риск 2 (90), допущение №2 (96). Выполнено способом 1 из трёх предложенных ревьюером r1 (сохранить пер-элементную защиту), усилено способами 2 (допущение переформулировано честно) и 3 (добавлен AC-свидетель) — все три сразу, не одно на выбор. |
| Low — «проёмов единицы» (К3 в старой нумерации) неточно | Число исправлено на «порядка тридцати на этаж» | Допущение №3 (строка 97): «Проёмов на `large-house` порядка тридцати на этаж, не «единицы»» |
Обе находки r1 закрыты текстом ТЗ, а не заявлением в комментарии — проверено построчным чтением текущего тела issue, а не принято на слово.
## Унаследовано из r1
Без повторной проверки принято из `docs/reviews/SPEC-REVIEW-534-r1.md`
(документ и SHA `41ae5e99868db27e3bfde9a40426c0a636c374bd`, дерево
материала `e87f2fb0f5a9…`), потому что дельта r2 этих участков текста и
кода не касается:
- Продуктовая рамка §7.1 (сценарий, «что человек увидит» до/после,
выбор полного трека по явно названному критерию §5, i18n/миграция/
бэкенд/touch как явные «нет» вместо молчания) — текст сценария в r2 не
менялся;
- AC1 (существующий смок остаётся зелёным без правок) — смок и его
проверки не менялись, вывод r1 держится;
- AC3/AC4 (численный ориентир 910 мс и полный перф-гейт из девяти
профилей) — числа и формулировка не менялись между раундами;
- AC6 (golden) и базовая часть AC7 (записи в оба changelog,
`User-Visible: yes`) — не менялись;
- Существование директивы `keyed` в зависимости `lit`, точность ссылок
на номера строк `houseplan-card.ts:11787/12996` и на существующие
мутанты в `scripts/mutation-gate.mjs` — репозиторий на этих участках
не менялся с r1, перепроверка кода не требовалась (я всё же прочитал
эти строки заново в рамках проверки новой находки ниже, они совпадают
с тем, что зафиксировал r1).
## Разбор AC (только то, чего касается дельта)
- **AC2а** (новая) — формулировка однозначна, способ доказательства
назван (`smoke`, вероятно расширение того же
`demo/smoke_space_switch_transitions.mjs`, который назван в
«Затронутые файлы»), критерий прохождения проверяем по идентичности
узла, как и AC2. Двусмысленности нет. **Но её область — только
маркеры устройств**, см. находку ниже.
- **AC5** — обновлена под новую форму кода (два мутанта: снимающий
внешний `keyed` и снимающий внутренний `repeat`). Явно называет
переанкеровку `device-markers-rendered-without-keys`. **Не называет**
`openings-rendered-without-keys`, хотя его якорь (`mutation-
gate.mjs:7441`, буквальная строка с `repeat(items, (o) => o.id, …)`)
тоже перестанет совпадать один-в-один после того, как К2 обернёт
список проёмов в `keyed(…)` — та же механика, что и для маркеров.
Практически это не тихий пропуск (`applyPatches` бросает исключение
на несовпавшем якоре, PROCESS §8/находка выше), но ТЗ не проговаривает
требование явно, оставляя его исполнителю угадывать.
- **AC7** — ссылка на «комментарий К3» согласована с новой нумерацией
контракта (проверено: К3 в новом тексте — это статья про
пер-элементный ключ, а не про число проёмов, как в r1). «Затронутые
файлы» требуют дополнить комментарий-ловушку **в обоих** файлах стилей
(`plan.styles.ts` и `devices.styles.ts`) — см. находку ниже про то, что
сам контракт К3 обосновывает это только для одного из двух.
## Находки
### Medium — К2 распространяет `keyed+repeat` на список проёмов «для единообразия», но обоснование (К3) и свидетель (AC2а) написаны только для маркеров устройств
**Что меняет К2.** «Оба списка — маркеры устройств и проёмы — рендерятся
как `keyed(<идентификатор пространства>, repeat(items, (item) =>
item.id, …))». Это новое требование к списку проёмов: до этого раунда
(и в r1, и сегодня в коде) проёмы рендерятся голым
`repeat(items, (o) => o.id, …)` без внешнего `keyed`. Причина
унификации — допущение №3: «проёмов… не заслуживают отдельного режима,
поэтому оба списка приводятся к одной форме» — обоснование
единообразием, а не функциональной необходимостью.
**К3 и AC2а покрывают только один из двух списков.** К3 («почему
пер-элементный ключ обязан остаться») целиком про маркеры: «Состав
списка маркеров меняется без смены пространства: призраки в редакторе
устройств (`showGhosts`) и появление устройства по живому синку
конфига… на `.device-shell-frame`… живы переходы… Убрать внутренний
ключ — значит вернуть тот же класс дефекта». AC2а — тоже буквально «свои
детель К3… **состав списка маркеров** меняется… ни один существующий
**маркер** не меняет свой DOM-узел». Ни К3, ни AC2а не упоминают проёмы.
**Тот же риск для проёмов подтверждён чтением, а не гипотетичен.**
1. Проёмы уже сегодня несут живые переходы того же типа, что маркеры, и
это задокументировано прямо в комментарии-ловушке #525
(`src/styles/plan.styles.ts:531-542`, тот самый абзац, который К2/К3
в этом раунде требуют дополнить): `.op-leaf { transition: transform
0.6s ease }`, `.op-arc { transition: stroke-dashoffset 0.6s ease }`.
Комментарий прямо называет обе причины рядом: «the openings and the
device markers… with this issue [#525]» — то есть исходная задача
#525 уже трактовала оба списка как один класс риска, и текущее ТЗ
(К2) следует этой логике для перфоманса, но не переносит её в
контракт корректности К3.
2. У проёмов есть собственный, отличный от маркеров, но настоящий
триггер изменения состава списка **внутри одного пространства**:
`_openingsR` (`src/houseplan-card.ts:8226-8260`) добавляет
orphan-запись, только когда `this._mode === 'plan'`
(`:8241-8243`), и убирает её, когда режим не `plan`
(`:8244`, `flatMap` возвращает `[]` для нерешённого хоста). То есть
переключение между режимами Plan/View **в одном и том же
пространстве** меняет длину и, соответственно, позиции элементов
`items`, который затем рендерится тем же `repeat` в
`_renderOpenings` (`:12987-12996`) — прямой аналог `showGhosts` для
маркеров, только с другим триггером.
**Почему это Medium, а не Low.** Если `repeat` для проёмов оставлен «по
инерции единообразия» без записанной причины и без свидетеля, следующий
читатель — ровно тот сценарий, которого боится сама К3 («иначе следующий
читатель уберёт «лишнюю» обёртку») — с равным основанием сочтёт его
избыточным именно для проёмов, потому что ни контракт, ни тест этого не
запрещают. Регрессия при этом ловится AC1/AC2 только частично: они
проверяют переключение *между* пространствами, а не смену режима
*внутри* одного. Находка того же класса и в той же строке контракта
(К2/К3), которую r1 уже поднимал для маркеров — только в r2 асимметрия
переехала на второй список, который сама эта правка впервые
подвела под общий режим.
**Что нужно для DoR (решает автор):**
1. распространить К3 и AC2а на оба списка одним общим утверждением
(единый триггер формулировки — «состав списка меняется без смены
пространства» верно и для orphan-переключения Plan/View) и завести
AC2б либо расширить AC2а на проёмы конкретным сценарием (например,
вход/выход из режима Plan при наличии проёма с нерешённым хостом); либо
2. явно обосновать, почему для проёмов регресс невозможен либо не имеет
значения (например, если `.op-leaf`/`.op-arc` не проигрывают
транзишн в застывшем Plan-виде, или если orphan-переключение всегда
сопровождается прочим ре-рендером, который сбрасывает состояние) — и
принять это как записанный риск, а не молчаливое умолчание;
3. как минимум явно назвать в AC5 переанкеровку
`openings-rendered-without-keys` наравне с `device-markers-
rendered-without-keys` — это не закрывает риск (1)/(2), но убирает
как минимум неполноту самого AC5 относительно кода, который сам же
К2 меняет.
**Решение ревьюера:** блокирует переход в `S5-ready` без правки одним из
способов 1 или 2 выше (способ 3 сам по себе недостаточен — устраняет
только неполноту AC5, но не закрывает риск отсутствия контракта/AC для
проёмов). Medium в скоупе задачи — К2 в этом самом раунде впервые
распространила изменение на список проёмов, поэтому решение относится к
предмету этой правки, а не к соседнему поведению; отдельный issue не
заводится (§2.4, §2.7, #202).
## Что проверено и корректно
- Обе находки r1 (Medium и Low) закрыты текстом ТЗ, проверено построчным
чтением, а не заявлением автора — таблица «Закрытие раунда r1» выше.
- Допущение №2 больше не путает конкретный фикс (#524, `box-shadow`) с
общим случаем: теперь это утверждение о производительности, а не о
безопасности, и корректность К3 от его истинности не зависит — `repeat`
защищает по ключу независимо от совпадения порядка.
- AC2а — новый, недвусмысленный AC с указанным способом доказательства.
- Нумерация контракта (К1–К5) внутренне согласована: перекрёстные ссылки
AC7→К3, AC5→AC2а, Риск 2→К3/AC2а/АС5 указывают на верные, актуальные
пункты нового текста.
- Технические ссылки К2 (директива `keyed`, номера строк 11787/12996,
существующие мутанты) точны — перепроверены на неизменном с r1 коде.
- Число проёмов («порядка тридцати на этаж») теперь соответствует
фикстуре `large-house` (было подтверждено ещё в r1).
## Чего не проверял
- Не запускал код и гейты — этап spec, продуктовый код не менялся с r1
(§2.4, §8), как и в прошлом раунде.
- Не проверял, действительно ли `.op-leaf`/`.op-arc` физически
проигрывают переход в статичном Plan-виде при переключении режима
(например, если элемент в момент переключения не виден или
transition подавлен другим правилом) — находка выше опирается на
чтение CSS и `_openingsR`, а не на измерение в браузере; это ровно тот
вопрос, который DoR-требование (пункт 2 находки) просит закрыть
автору чтением/измерением, если он выберет этот путь вместо пункта 1.
- Не проверял частоту реального использования Plan/View-переключения с
ровно одним orphan-проёмом «в проде» — как и в r1 по аналогичному
вопросу для маркеров, для решения находки (правка контракта либо
обоснованный отказ) это не требуется: сценарий не исключён кодом и не
покрыт тестом, этого достаточно, чтобы вернуть на уточнение.
## Вердикт
Жёлтый. Один Medium в скоупе задачи: К2 в этом раунде распространила
`keyed(space.id, repeat(…))` на список проёмов «для единообразия», но
контракт К3 и его свидетель AC2а обосновывают и проверяют сохранение
внутреннего ключа только для маркеров устройств, хотя тот же класс риска
у проёмов подтверждён чтением (комментарий-ловушка #525 уже описывает их
как один класс, а `_openingsR` меняет состав списка при переключении
Plan/View внутри одного пространства). High нет. Обе находки r1 закрыты
текстом, проверено построчно. Возврат автору на правку одним из двух
названных способов (расширить К3/AC2а на проёмы, либо обосновать и
записать отказ как риск).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `e40d3f18d2fd` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `d42689f768439e92dac62df2f8055b5f2ed7d1db`
```
git log --all --format='%H %T' | grep d42689f76843
```
- Тело issue: `e9c78bcaf2429aff824dd80820d9b17d1a73c4b49c557df613d54a59d5590907`
- Вердикт конвейера: `yellow` · High 0
+218
View File
@@ -0,0 +1,218 @@
# SPEC-REVIEW-534-r3
## Скоуп
Issue #534 (`bug`, `P1`, полный трек — критерий §5 «нет влияния на
производительность» явно назван нарушенным в самой ТЗ). Этап `spec`
(ревью ТЗ, PROCESS.md §2.4), заход **r3**, до этого раунда израсходовано
**2/4** блокирующих циклов (r1 — жёлтый/Medium, r2 — жёлтый/Medium; оба в
скоупе, оба вернули задачу на правку). Лимит цикла для полного трека — 4
(§2.4), не 2 (2 — это лимит именно лёгкого трека).
Предмет спецификации не изменился с r1: регресс #525 (перевод списков
маркеров и проёмов на `repeat(items, (x) => x.id, …)`) добавил лишний диф
по непересекающимся ключам при смене пространства на `large-house`/`plan-
snap`; решение — обернуть оба списка во внешний `keyed(space.id, …)`,
сохранив внутренний `repeat` как защиту от переупорядочивания внутри
пространства.
## Как проверялось (объём по дельте, §2.9/§2.10)
Материал предыдущего раунда объявлен в блоке «Материал раунда» документа
`docs/reviews/SPEC-REVIEW-534-r2.md`: коммит `e40d3f18d2fd`, дерево
`d42689f768439e92dac62df2f8055b5f2ed7d1db`, тело issue
`sha256:e9c78bcaf2429aff824dd80820d9b17d1a73c4b49c557df613d54a59d5590907`.
1. Текущее тело issue снято `gh issue view 534 --json body -q '.body'` →
`/tmp/current_body.md`, посчитан хеш:
`sha256:18c0ed92ea4d832fcb8d3d4aeedf02d05af2e4c174d0f7e0b099d4b14e31ff8a`
— отличается от хеша r2, дельта в теле issue подтверждена, а не
предполагается.
2. Дельта — правка тела issue владельцем, описанная в комментарии
«Ответ на ревью ТЗ r2 — заход 3» (2026-09-11T19:09:03Z): К3 расширен на
оба списка (маркеры и проёмы), AC2а расширен второй половиной для
проёмов, AC5 явно называет переанкеровку обоих мутантов, Риск 2
называет оба триггера. Сверено построчно с текущим телом issue — все
перечисленные правки на месте (К3 — строки 50–53 тела, AC2а — 65–69,
AC5 — 72, Риск 2 — 99).
3. Продуктовый код на материале ревью (рабочая копия на `1fb84755`, тем
же SHA, что был у r1/r2 — код с r1 не менялся, это ревью ТЗ) прочитан
заново там, где дельта делает новые фактические утверждения:
- `src/houseplan-card.ts:8226-8258` (`_openingsR`) — подтверждено
построчно: `this._mode === 'plan' ? [{...fallback, orphanReason}] :
[]`. К3/AC2а формулируют это как «Plan ↔ View»; в коде условие шире
— `plan` против любого из **трёх** остальных режимов (`view`,
`devices`, `decor`, `houseplan-card.ts:990`). «Plan ↔ View» —
корректный частный случай общего триггера, демонстрирует и
проверяет тот же механизм (изменение состава `items` без смены
`space.id`); не искажает ни один AC и не расширяет риск — см. Low
ниже;
- `src/styles/plan.styles.ts:531-542` — комментарий-ловушка #525
прочитан целиком: один блок на оба списка, сегодня называет
`.op-leaf`/`.op-arc` (проёмы) и `.device-shell-frame, box-shadow`
(маркеры, устаревшая причина — box-shadow снят #524). К3/АС7
корректно требуют его переписать под новые триггеры и актуальные
переходы;
- `src/styles/devices.styles.ts:213` — `.device-shell-frame {
transition: border-color .15s, opacity .2s; }` — переходы живы,
как и утверждает К3 (маркеры);
- `src/houseplan-card.ts:11787` (`repeat(devs, …)`) и `:12996`
(`repeat(items, …)`) — оба списка сегодня без внешнего `keyed`,
совпадает с описанием целевого состояния К2;
- `node_modules/lit/directives/keyed.js` — директива по-прежнему
доступна в установленной зависимости.
Гейты не гонялись — этап spec, продуктовый код не менялся с r1 (§2.4,
§8), как и в r1/r2.
## Закрытие раунда r2
| находка r2 | чем закрыта | где это видно в текущем теле issue |
|---|---|---|
| Medium — К2 распространила `keyed(space.id, repeat(…))` на проёмы «для единообразия», но контракт К3 и свидетель AC2а обосновывали и проверяли сохранение внутреннего ключа только для маркеров устройств; тот же риск для проёмов подтверждён чтением (`_openingsR`, комментарий-ловушка #525) | К3 переписан заголовком «Почему пер-элементный ключ обязан остаться — **в обоих списках**» и содержит отдельный абзац «Проёмы» с тем же обоснованием, что привёл ревьюер: `_openingsR` включает orphan-запись только в режиме Plan, `.op-leaf`/`.op-arc` несут переходы `transform 0.6s`/`stroke-dashoffset`. AC2а расширен второй половиной сценария «проёмы: переключение Plan ↔ View при наличии проёма с нерешённым хостом». AC5 явно называет переанкеровку **обоих** существующих мутантов — `device-markers-rendered-without-keys` **и** `openings-rendered-without-keys`. Риск 2 переписан: «Триггеров два и они разные: для маркеров… для проёмов…», ссылается на «обе половины AC2а». | Тело issue: К3 (строки 50–53), AC2а (65–69), AC5 (72), Риск 2 (99). Выполнены сразу способ 1 (расширить К3/AC2а) и способ 3 (явно назвать оба мутанта в AC5) из трёх, предложенных r2 — не выбран один вариант, закрыты оба одновременно. |
Находка закрыта текстом ТЗ, проверено построчным чтением текущего тела
issue, а не заявлением автора в комментарии.
## Унаследовано из r1 и r2
Без повторной проверки принято из `docs/reviews/SPEC-REVIEW-534-r1.md`
(SHA `41ae5e99868db27e3bfde9a40426c0a636c374bd`) и
`docs/reviews/SPEC-REVIEW-534-r2.md` (SHA `e40d3f18d2fdb1bc5f2f9f6dec969c490cddde1f`),
поскольку дельта r3 этих участков текста не касается:
- Продуктовая рамка §7.1 (сценарий, «что человек увидит» до/после, выбор
полного трека по явно названному критерию §5, i18n/миграция/бэкенд/
touch как явные «нет») — не менялась с r1.
- К1 («узел не переживает смену пространства») и К4/К5 (бюджет, вне
объёма) — не менялись с r1.
- AC1 (существующий смок остаётся зелёным без правок), AC3/AC4
(численный ориентир 910 мс, полный перф-гейт на 9 профилях), AC6
(golden), базовая часть AC7 (записи в оба changelog, `User-Visible:
yes`) — числа и формулировки не менялись между r1/r2/r3.
- Существование директивы `keyed` в зависимости `lit`, точность ссылок
на номера строк `houseplan-card.ts:11787/12996` и на существующие
мутанты в `scripts/mutation-gate.mjs:7434-7456` — репозиторий на этих
участках не менялся с r1 (перепроверено заново в этом раунде наравне с
новыми утверждениями К3, см. «Как проверялось» — совпадает с r1/r2).
- Допущение №1 (`keyed` гарантированно выбрасывает поддерево) и №2
(внутренний `repeat` идёт по быстрому пути внутри пространства,
измерено 905,9 vs 903,7 мс) — не менялись с r2.
## Разбор AC (только то, чего касается дельта r3)
- **К3** — теперь однозначно покрывает оба списка отдельными абзацами с
названным триггером, named CSS-классом и переходом для каждого.
Двусмысленности, кто из двух списков защищён, а кто нет, больше нет.
- **AC2а** — обе половины (маркеры/проёмы) сформулированы параллельно,
способ доказательства общий (`smoke`), критерий прохождения проверяем
по идентичности узла и отсутствию анимации для каждого списка отдельно.
Единственное найденное уточнение — сценарий проёмов назван «Plan ↔
View», хотя код переключает orphan-запись по условию «`plan` vs любой
из трёх остальных режимов»; см. Low ниже, не блокирует.
- **AC5** — оба существующих мутанта названы по имени, требование
переанкеровки для обоих явное, не подразумеваемое. Неполнота,
найденная в r2 (`openings-rendered-without-keys` не назван), закрыта.
- **AC7** — «комментарий К3 стоит рядом с анимациями» (множественное
число) теперь согласовано с К3, описывающим анимации для обоих
списков; «Затронутые файлы» верно называют оба файла стилей
(`plan.styles.ts`, где сегодня живёт единственный комментарий-ловушка
на оба списка, и `devices.styles.ts`, рядом с самим
`.device-shell-frame`) — технический вопрос «один комментарий или два»
ТЗ не обязано решать (не наблюдаемо пользователем, решает автор по
§7.1 «всё, чего пользователь не наблюдает»).
Остальные AC (AC1, AC3, AC4, AC6) дельта не задевает — наследуются из
r1/r2 без повторной проверки.
## Находки
Блокирующих (High/Medium) находок нет.
### Low — АС2а/К3 называют триггер проёмов «Plan ↔ View», код переключает шире
`_openingsR` (`src/houseplan-card.ts:8241-8244`) убирает orphan-запись
при **любом** режиме, отличном от `plan` (`view`, `devices`, `decor`), не
только при `view`. «Plan ↔ View» — корректный и достаточный частный
случай для смока: механизм (изменение состава `items` без смены
`space.id`) один и тот же независимо от того, куда именно переключились
из `plan`. Ни один AC от более узкой формулировки не становится
неверным или непроверяемым — переключение именно в `view` даёт тот же
эффект, что и в `devices`/`decor`.
**Решение ревьюера:** не блокирует. Формулировка достаточна для
однозначного и проверяемого AC; если исполнитель захочет расширить смок
на все три режима — это усиление сверх ТЗ, не требование ревью.
## Что проверено и корректно
- Единственная Medium-находка r2 закрыта текстом ТЗ, проверено построчным
чтением текущего тела issue (не заявлением автора) — таблица «Закрытие
раунда r2» выше.
- Технические факты нового текста К3 (переходы `.op-leaf`/`.op-arc`,
условие `_openingsR` на `this._mode === 'plan'`, живые переходы
`.device-shell-frame`) подтверждены прямым чтением исходников на
материале ревью, а не приняты на слово.
- AC2а, AC5, Риск 2, «Затронутые файлы» внутренне согласованы между
собой и с новым текстом К3 — перекрёстные ссылки указывают на верные
актуальные пункты.
- Нумерация контракта (К1–К5) и AC (включая AC2а) не разъехалась между
раундами.
- Бюджет циклов не превышен: это второй жёлтый цикл из четырёх
возможных для полного трека (§2.4), лимит 4, использовано было бы 2 —
но этот раунд, если зелёный, бюджет не тратит (§4, #227).
## Чего не проверял
- Не запускал код и гейты — этап spec, продуктовый код не менялся с r1
(§2.4, §8).
- Не проверял в браузере, действительно ли переключение `plan → devices`
или `plan → decor` (а не только `plan → view`) физически воспроизводит
тот же дефект — риск в Low-находке выше опирается на чтение условия
`_openingsR`, а не на измерение; для вывода «AC2а достаточен» это не
требуется, поскольку механизм (состав `items` меняется без смены
`space.id`) один и тот же для всех трёх направлений.
- Не проверял частоту реальных сценариев `showGhosts`/лайв-синка/
Plan-Х-переключения «в проде» количественно — не требуется для этого
раунда, вопрос был закрыт ещё в r1/r2 (сценарий кодом не исключён и не
покрыт тестом — этого достаточно, чтобы Medium r2 был обоснованным, а
его закрытие текстом ТЗ — достаточным).
## Вердикт
**Зелёный.** Обе предыдущие Medium-находки (r1 — допущение о безопасности
внутрипространственного переиспользования маркеров, r2 — асимметрия
контракта К3/AC2а между маркерами и проёмами) закрыты текстом ТЗ,
проверено построчным чтением, а не заявлением автора. High нет. Одна
Low-находка (формулировка триггера проёмов у`́же реального условия кода)
не блокирует и не требует правки. Готово к разработке.
---
<!-- material-anchors: подготовлено вручную ревьюером -->
## Материал раунда
- Ветка: `dev`, коммит `1fb84755c22ba1fe990a96936399b579f0835a20` (рабочая
копия на этом SHA; код с r1 не менялся, это ревью ТЗ).
- Тело issue (нормализовано `gh issue view 534 --json body -q '.body'`):
`sha256:18c0ed92ea4d832fcb8d3d4aeedf02d05af2e4c174d0f7e0b099d4b14e31ff8a`
- Предыдущий материал: r1 — коммит `41ae5e99868db27e3bfde9a40426c0a636c374bd`,
тело `sha256:98e12a0f7bec0c050802b1caee9c139db3bbc7b7c1169744fb1ccd567e35babd`;
r2 — коммит `e40d3f18d2fdb1bc5f2f9f6dec969c490cddde1f`,
тело `sha256:e9c78bcaf2429aff824dd80820d9b17d1a73c4b49c557df613d54a59d5590907`.
- Вердикт: `green` · High 0 · Medium 0 · Low 1 (не блокирует)
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `1fb84755c22b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `b5414fcc6281268a81bae04b9905f7199afa6242`
```
git log --all --format='%H %T' | grep b5414fcc6281
```
- Тело issue: `9d969d777f9787e5a50d02b2e675ad1448e945a27b47899d2d37c445a612f30e`
- Вердикт конвейера: `green` · High 0
+17 -4
View File
@@ -2400,6 +2400,18 @@ const MUTANT_DEFINITIONS = [
replace: "const HANDLE_PAINT_ORDER = ['corners', 'edges'] as const;",
}],
},
{
id: 'daycycle-outline-not-promoted',
guard: 'node demo/smoke_daycycle_raster.mjs',
because: 'the day-cycle outline is a triple drop-shadow over the whole sheet; without its own '
+ 'compositing layer every repaint of the plan re-runs three blur passes, and a demo-stand '
+ 'pan costs fifteen times the rasterization of a static background (#532 AC1/AC2)',
patches: [{
file: 'src/styles/plan.styles.ts',
find: ' will-change: filter;\n',
replace: '',
}],
},
{
id: 'align-guides-exclude-dead-source',
guard: 'node demo/smoke_align_guides.mjs',
@@ -7426,8 +7438,9 @@ const MUTANT_DEFINITIONS = [
+ 'animates a door that never moved (#525 AC1)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' return svg`<g class="openinglayer">${repeat(items, (o) => o.id, (o) => {',
replace: ' return svg`<g class="openinglayer">${items.map((o) => {',
find: " return svg`<g class=\"openinglayer\">${keyed(space.id, repeat(items,"
+ " (o) => o.id, (o) => {",
replace: " return svg`<g class=\"openinglayer\">${keyed(space.id, items.map((o) => {",
}],
},
{
@@ -7438,8 +7451,8 @@ const MUTANT_DEFINITIONS = [
+ 'painter, which finds the dragged marker by that attribute (#525 AC2)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' ${repeat(devs, (d) => d.id, (d) => this._renderDevice(',
replace: ' ${devs.map((d) => this._renderDevice(',
find: ' ${keyed(space.id, repeat(devs, (d) => d.id, (d) => this._renderDevice(',
replace: ' ${keyed(space.id, devs.map((d) => this._renderDevice(',
}],
},
{
+5 -4
View File
@@ -9,6 +9,7 @@
import { LitElement, html, svg, nothing, noChange, TemplateResult, PropertyValues, type PropertyDeclaration } from 'lit';
import { cache as litCache } from 'lit/directives/cache.js';
import { guard } from 'lit/directives/guard.js';
import { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js';
import './hp-dialog';
import type { HpDialog } from './hp-dialog';
@@ -11784,9 +11785,9 @@ export class HouseplanCard extends LitElement {
view,
(point) => this._scenePoint(point),
)}
${repeat(devs, (d) => d.id, (d) => this._renderDevice(
${keyed(space.id, repeat(devs, (d) => d.id, (d) => this._renderDevice(
d, view, showLqi, isoOverlays?.devices.get(d.id),
))}
)))}
${this._renderVacuums(this._renderVacuumDevices, view, space.id)}
${this._renderVacFit(view)}
${this._renderOpeningLocks(view, isoOverlays?.locks)}
@@ -12993,7 +12994,7 @@ export class HouseplanCard extends LitElement {
const walls = this._spaceWalls;
const openCuts = this._openCuts();
const openingWallIndex = this._openingWallIndexFor(space, openCuts).value;
return svg`<g class="openinglayer">${repeat(items, (o) => o.id, (o) => {
return svg`<g class="openinglayer">${keyed(space.id, repeat(items, (o) => o.id, (o) => {
if (o.orphanReason) return svg`<g class="opening orphan" data-hp="opening-orphan"
data-id=${o.id} role="button" tabindex="0"
aria-label=${this._t('opening.partition_orphan')}
@@ -13045,7 +13046,7 @@ export class HouseplanCard extends LitElement {
@pointerup=${(e: PointerEvent) => this._opPointerUp(e, o)}
@pointercancel=${(e: PointerEvent) => this._opPointerUp(e, o)}></rect>
</g>`;
})}</g>`;
}))}</g>`;
}
/** Padlock badges for door-like openings with a lock entity. */
+3
View File
@@ -210,6 +210,9 @@ export const devicesStyles = css`
background: transparent;
box-shadow: var(--device-shell-shadow);
/* no box-shadow here: cqw-sized, restarts on every container resize (#524) */
/* these two are why the marker list keeps a per-item key under the
space-level one (#525, #534): ghosts and live config sync shift
positions inside one space — see plan.styles.ts for the full note */
transition: border-color .15s, opacity .2s;
pointer-events: auto;
/* Normative production fallback: never add a per-marker backdrop blur. */
+20 -1
View File
@@ -86,12 +86,21 @@ export const planStyles = css`
opacity 1100ms cubic-bezier(.22, .61, .36, 1);
}
.stage.mode-transition .hp-day-cycle-env { transition: none; }
/* #532: the outline keeps its OWN compositing layer. The group holds paper
silhouettes only and does not change on hover or pan, but the filter used
to live in the plan's own layer — so every repaint of the plan re-ran
three blur passes over the whole sheet. Measured on a demo-stand pan
(Chromium, CDP, summed RasterTask): 852 ms without the hint against 28 ms
with it, where a static background costs 108 ms. The hint costs one layer
of memory, so it is scoped to .daycycle and a static background never
pays for it. */
.stage.daycycle .hp-paperg,
.hp-static-stage.daycycle .hp-paperg {
filter:
drop-shadow(0 0 1px var(--hp-day-cycle-outline-near))
drop-shadow(0 0 5px var(--hp-day-cycle-outline-mid))
drop-shadow(0 0 10px var(--hp-day-cycle-outline-far));
will-change: filter;
transition: filter 1100ms cubic-bezier(.22, .61, .36, 1);
}
@media (prefers-reduced-motion: reduce) {
@@ -530,7 +539,17 @@ export const planStyles = css`
Anything animated that is rendered from a list needs a key. The Glow
spots learned it first (glow-scene.ts, opacity), the openings and the
device markers (.device-shell-frame, box-shadow) with this issue.
Witness: demo/smoke_space_switch_transitions.mjs. */
#534: the shape is now keyed(space.id, repeat(items, (o) => o.id, ...)),
and BOTH halves are load-bearing. The outer key does the space switch:
there the key sets are disjoint, so repeat would build two key maps and
walk both lists only to throw everything away — 80 ms of it on a plan
with two hundred markers. The inner key does everything inside one
space, where the list composition still changes underneath: an opening
with an unresolved host is listed ONLY in plan mode (_openingsR), so
entering and leaving the editor shifts the positions of the rest.
Removing either half brings the phantom animation back through a
different door. Witnesses: demo/smoke_space_switch_transitions.mjs. */
.op-leaf {
transition: transform 0.6s ease;
}
+4 -1
View File
@@ -23,7 +23,10 @@ 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': 13650,
// 2026-09-11, #534: +1 for `import { keyed }`. The line buys back the 80 ms
// that #525 put on every space switch, and there is nothing to move out —
// the render itself did not grow by a character.
'src/houseplan-card.ts': 13651,
// #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.