refactor(editor): диалог «Оптимизировать планы» — свой модуль с узким портом (#642)

Первый вынос из монолита по образцу live-*/RadarSetupController.
src/optimize-plans-dialog.ts: OptimizePlansDialogState, чистые
preflightDiagnostics/preflightVersionsDiffer и класс OptimizePlansDialog
(open/preview/toggleLivePositions/run/copyDiagnostics/
reportPreflightFailure/render, clipboardFallback) за портом из 18 членов.
Модуль не знает HouseplanEditorHostPort; импортирует его только рантайм,
карточка берёт тип.

Инлайн-фолбэк буфера обмена — WeakMap по объекту диалога
(CODE-REVIEW-295-r1 M2), дедуп dev-лога — поле класса: из карточки и порта
ушли _preflightClipboardFallback и _reportedPreflightFingerprint, пять
делегатов и две стрелки-заглушки. Харнесс (13 смоков, wall-draw-click,
golden) зовёт card._editorRuntime.optimizePlans.* — меняется только адрес.

11 текстовых утверждений i18n.test.mjs о разметке и тосте диалога и гварды
8 мутантов переехали в юнит test/optimize-plans-dialog.test.mjs
(test-build); новый мутант optimize-dialog-imports-host-port.

monolith-baseline: delegates 159→154, portMembers 350→348,
hostRefs 4948→4869, portPrivates 96→94, harnessPrivates 107→101,
bundleBytes 2499647→2499182. Потолки ядер опущены на выигрыш;
потолок lazy editor gzip пересчитан (+555 Б gzip при −465 Б сырых).

Issue: #642
User-Visible: no
This commit is contained in:
Claude
2026-09-24 08:00:58 +00:00
committed by claude[bot]
parent 79ee16546d
commit 66ae952c2a
70 changed files with 1833 additions and 1208 deletions
@@ -1,127 +1,127 @@
{
"schema": 1,
"fingerprint": "be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6",
"fingerprint": "d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 289449,
"initialViewGzipBytes": 289273,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 290971,
"initialPanelGzipBytes": 290796,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2324,
"lazyFiles": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/de-CjsIUY-d.js",
"houseplan-assets/editor-Dv3uqeSS.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/fr-D2W98VDc.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js",
"houseplan-assets/hp-zigbee-topology-overlay-C5T09STo.js",
"houseplan-assets/iso-scene-render-3HN3TF4R.js",
"houseplan-assets/live-interaction-runtime-kRE37k2_.js",
"houseplan-assets/pdf-export-DKVHRlMO.js",
"houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js",
"houseplan-assets/zigbee-topology-runtime-C4WynS64.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/de-C_-FOd3R.js",
"houseplan-assets/editor-CuOqOWa_.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/fr-BubWZSkS.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js",
"houseplan-assets/hp-zigbee-topology-overlay-DQp1tIHa.js",
"houseplan-assets/iso-scene-render-DpO3ezuP.js",
"houseplan-assets/live-interaction-runtime-CPQNHLqJ.js",
"houseplan-assets/pdf-export-CFT_MzmM.js",
"houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js",
"houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js"
],
"lazyGzipBytes": 460916,
"lazyGzipBytes": 461471,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/editor-Dv3uqeSS.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/editor-CuOqOWa_.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js"
],
"lazyEditorGzipBytes": 244420,
"lazyEditorGzipBytes": 244976,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js"
],
"lazyOnboardingGzipBytes": 34521,
"lazyOnboardingGzipBytes": 34523,
"lazyLocaleFiles": [
"houseplan-assets/de-CjsIUY-d.js",
"houseplan-assets/fr-D2W98VDc.js"
"houseplan-assets/de-C_-FOd3R.js",
"houseplan-assets/fr-BubWZSkS.js"
],
"lazyLocaleGzipBytes": 55255,
"lazyLocaleGzipBytes": 55257,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-3HN3TF4R.js"
"houseplan-assets/iso-scene-render-DpO3ezuP.js"
],
"lazyIsometricGzipBytes": 17640,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js"
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js"
],
"lazyFurnitureArtGzipBytes": 16940,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/pdf-export-DKVHRlMO.js"
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/pdf-export-CFT_MzmM.js"
],
"lazyPdfGzipBytes": 126827,
"lazyPdfGzipBytes": 126826,
"files": [
{
"path": "houseplan-assets/backdrop-pick-YbdnEEmM.js",
"sha256": "07acc6fc3e12ba2fe3e1b07e19a3f57308a0834a03db3c01bdba22ea43b9a7fd",
"path": "houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"sha256": "37be8583306fa604aed0f7f79f7a6c3eb8e373de8e5d476fb4593881b7954f93",
"rawBytes": 76524,
"gzipBytes": 24571,
"gzipBytes": 24572,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CjsIUY-d.js",
"sha256": "25fa2d8f19df083532e8f022d86c21d290f3e79badaf7b4d058158c50a02286c",
"path": "houseplan-assets/de-C_-FOd3R.js",
"sha256": "033535ad61de5d4453c3b3729cf2afb69ed996267cbb83ebd2111b0ce6cad359",
"rawBytes": 92247,
"gzipBytes": 27864,
"gzipBytes": 27865,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Dv3uqeSS.js",
"sha256": "b1cdd26dec11ae7d904a675fbc27964757819886d1034ab63d2c9d91b42e3655",
"path": "houseplan-assets/editor-CuOqOWa_.js",
"sha256": "350885825a2046833dcef868485cae1efe99389d088fd502bd166d1ecc75b2a7",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1581,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"sha256": "aee7494545b43e2c932163c572b19483d9ac35c022daf437bcfd3aa21589aa07",
"path": "houseplan-assets/form-kit.styles-ORjqyA2O.js",
"sha256": "c6899e84bdfc9d83f635e63317260c6e3e0ae85104d966e0e297d56abcf45d91",
"rawBytes": 25600,
"gzipBytes": 5055,
"gzipBytes": 5056,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-D2W98VDc.js",
"sha256": "12e70d1138d31d87f8c7f25d750bef3ecb7c84e23925845c101f0af4298208c0",
"path": "houseplan-assets/fr-BubWZSkS.js",
"sha256": "6243e53b2d25b50f23e7b8544cf24ae2634f3d985d0ebb05ad881628121693f4",
"rawBytes": 94579,
"gzipBytes": 27391,
"gzipBytes": 27392,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"sha256": "63e67cda0c59f37fee93410ae4193ae79d730b83293eea331edefaf63e280d11",
"path": "houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"sha256": "aac82444a5129c8991694a9a5421a63fc23479d41ed2cf44be2bba9cdb8bac38",
"rawBytes": 52035,
"gzipBytes": 16940,
"isEntry": false,
@@ -129,184 +129,184 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-e9vxRZcR.js",
"sha256": "98c44bf6f915d6121132c133cc12b2e27ea344f1c8203e7af54741f317aa1a5e",
"rawBytes": 1022206,
"gzipBytes": 288647,
"path": "houseplan-assets/houseplan-card-C2ifrS7A.js",
"sha256": "6e2ecbe962122cfa7599c75e5ca8815d27c98b754cc437b7628b4ddafef10d9f",
"rawBytes": 1021428,
"gzipBytes": 288472,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CjsIUY-d.js",
"houseplan-assets/editor-Dv3uqeSS.js",
"houseplan-assets/fr-D2W98VDc.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js",
"houseplan-assets/hp-zigbee-topology-overlay-C5T09STo.js",
"houseplan-assets/iso-scene-render-3HN3TF4R.js",
"houseplan-assets/live-interaction-runtime-kRE37k2_.js",
"houseplan-assets/pdf-export-DKVHRlMO.js",
"houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js"
"houseplan-assets/de-C_-FOd3R.js",
"houseplan-assets/editor-CuOqOWa_.js",
"houseplan-assets/fr-BubWZSkS.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js",
"houseplan-assets/hp-zigbee-topology-overlay-DQp1tIHa.js",
"houseplan-assets/iso-scene-render-DpO3ezuP.js",
"houseplan-assets/live-interaction-runtime-CPQNHLqJ.js",
"houseplan-assets/pdf-export-CFT_MzmM.js",
"houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"sha256": "f476a861b5a2d77cd2ecea724c57d4e9e6dce6fa161eb18cc9479d2ec9752641",
"rawBytes": 678218,
"gzipBytes": 188846,
"path": "houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"sha256": "7bad833f2fd60af11c8411e56b97431c71ee3e4fff685de3019a9ac14e5bc1ba",
"rawBytes": 678531,
"gzipBytes": 189399,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-C4WynS64.js"
"houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js",
"sha256": "5613ece2554c3ee9afc3e71dce63370d3e0bbeab9489e297ffaee74f33670e6f",
"path": "houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js",
"sha256": "1a16adf7c3ab18db3a9773930103843470acdeb2fde3870110c20a069cd28876",
"rawBytes": 15986,
"gzipBytes": 4895,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-C5T09STo.js",
"sha256": "d836d918b89ee5685f7d2d10b194cf17feb7ca956c2112c78c4d62db7042ef24",
"path": "houseplan-assets/hp-zigbee-topology-overlay-DQp1tIHa.js",
"sha256": "13cf97cdc63e8b66108521655d6e4585452fcf02ce9f4fc0b0bf19c13cc6f099",
"rawBytes": 10759,
"gzipBytes": 3686,
"gzipBytes": 3685,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js",
"houseplan-assets/zigbee-topology-runtime-C4WynS64.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js",
"houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-3HN3TF4R.js",
"sha256": "0c1137699d170125b0b3a6a7dd5177a06e270d74e8b3020711c100b4c59bbe0d",
"path": "houseplan-assets/iso-scene-render-DpO3ezuP.js",
"sha256": "c979378b329bca0dbd23ddbae6ca2cad763bb5754959e9b98f395197d915a8b5",
"rawBytes": 51983,
"gzipBytes": 17640,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-kRE37k2_.js",
"sha256": "e0537b8b7d78df3e70208e5601b95b538509265184027a62b559c43626372385",
"path": "houseplan-assets/live-interaction-runtime-CPQNHLqJ.js",
"sha256": "10e83f4eb0bdf1e4b0b0a4c9e377ade059bf5169488fa360821625a4df4bc91f",
"rawBytes": 8216,
"gzipBytes": 3203,
"gzipBytes": 3206,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-DKVHRlMO.js",
"sha256": "fef91b049f58622de45132f31b3ab7f606df5db8eee3fc68d3809b2c5ece7e5d",
"path": "houseplan-assets/pdf-export-CFT_MzmM.js",
"sha256": "03ee0fc3e88bc84e94c1a51fadc2e65a17635ba53ed875f1cdb794c3eb989106",
"rawBytes": 236264,
"gzipBytes": 109887,
"gzipBytes": 109886,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-Bj45k8M3.js",
"sha256": "1463acf70394eb7c8fc09b8182b227648b00919bb2fc5ad6a6ae189b1a051a2e",
"path": "houseplan-assets/summary-panel-editor-11x1Zz63.js",
"sha256": "2d4be3e90f3c0329c33b6465d57c82fc2ba93e30097838cd042c69f0cc0f08be",
"rawBytes": 14862,
"gzipBytes": 3423,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js"
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-3K7HlQXp.js",
"sha256": "246a3ee30014c9096527044921c104b76cef9555ebd1b2fbfcaeea54daf010cc",
"path": "houseplan-assets/summary-panel-metrics-BwUdppyx.js",
"sha256": "5dbb467356ea155e5d4dee21c562cbfdf0a763b93d728f8227f7731b91837068",
"rawBytes": 2611,
"gzipBytes": 1306,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js",
"sha256": "527c40315a7cbbfa84289d6c8360688df121f368bf2609488e0b3b527ed2988a",
"path": "houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js",
"sha256": "6e0e9808f1da372aef041d74e46c91e29d2d5fd98a9fbe24ee54debe58c06ee3",
"rawBytes": 72637,
"gzipBytes": 20166,
"gzipBytes": 20163,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-Bj45k8M3.js",
"houseplan-assets/summary-panel-metrics-3K7HlQXp.js"
"houseplan-assets/summary-panel-editor-11x1Zz63.js",
"houseplan-assets/summary-panel-metrics-BwUdppyx.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-CXRVgVl2.js",
"sha256": "a68916a24db8c6acc389707ad40f95d8963d4521e40c09e6fa32b6430584569a",
"path": "houseplan-assets/zigbee-topology-BIziY8_4.js",
"sha256": "be1428736b1fcd653bec4a452d4895d8cfea015cc143b8560f5c6065c8b6ffea",
"rawBytes": 18962,
"gzipBytes": 7425,
"gzipBytes": 7428,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-C4WynS64.js",
"sha256": "598d78eb03a6b75e7bf9ce8ac7fdbc7ee514d6c7f056e8651309c541313ced0e",
"path": "houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js",
"sha256": "af36ce4a7b2e9b6992e2c74a19e63f197a1960317fad3bb8f193d93ccccb84b8",
"rawBytes": 3942,
"gzipBytes": 1764,
"gzipBytes": 1763,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "ff5753ceb4dce57c6f1701baad303c37770735cb53b2a61c14e1225e07ac898b",
"sha256": "9a30c8176149f7b6b78431e5f876dd8ab3f86676bf31ecfbd8952821812a46ed",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 801,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "90fb829ea2a8310205a7bc7c8d5d57bd38eedb0d62c961eed13d3bc3119b011a",
"sha256": "e712ffa4a818f355510e4eee5ed9438c6f89ac60797f0838bba4b2298cb35d24",
"rawBytes": 6165,
"gzipBytes": 2324,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{A as e,c as a,s as t,l as i,d as n,ap as s,h as r,j as o,g as l,dH as c,dI as p,dJ as d,cL as h,dK as u,cK as m,dL as g,cQ as _,cP as b,dM as f,bs as v,br as w,bt as y,av as k,aC as $,aD as x,dN as S}from"./houseplan-card-e9vxRZcR.js";import{c as C,f as M,C as L}from"./form-kit.styles-Dlhk9WKB.js";const z=C+M(L,{withSwitch:!1}),P=new WeakSet;let D=null;function T(e){const a=e.renderRoot;if(!a||P.has(a))return;const t=a.ownerDocument??globalThis.document,i=t?.defaultView?.CSSStyleSheet;if(i&&"adoptedStyleSheets"in a)return D||(D=new i,D.replaceSync(z)),a.adoptedStyleSheets=[...a.adoptedStyleSheets,D],void P.add(a);if(a.querySelector("style[data-hp-form-kit]"))return void P.add(a);const n=t.createElement("style");n.dataset.hpFormKit="true",n.textContent=z,a.appendChild(n),P.add(a)}function H({title:t,help:i,body:n,id:s}){return a`<section class="hpf-card" data-card=${s??e}>
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{A as e,c as a,s as t,l as i,d as n,ap as s,h as r,j as o,g as l,dH as c,dI as p,dJ as d,cN as h,dK as u,cM as m,dL as g,cS as _,cR as b,dM as f,bt as v,bs as w,bu as y,av as k,aC as $,aD as x,dN as S}from"./houseplan-card-C2ifrS7A.js";import{c as C,f as M,C as L}from"./form-kit.styles-ORjqyA2O.js";const z=C+M(L,{withSwitch:!1}),P=new WeakSet;let D=null;function T(e){const a=e.renderRoot;if(!a||P.has(a))return;const t=a.ownerDocument??globalThis.document,i=t?.defaultView?.CSSStyleSheet;if(i&&"adoptedStyleSheets"in a)return D||(D=new i,D.replaceSync(z)),a.adoptedStyleSheets=[...a.adoptedStyleSheets,D],void P.add(a);if(a.querySelector("style[data-hp-form-kit]"))return void P.add(a);const n=t.createElement("style");n.dataset.hpFormKit="true",n.textContent=z,a.appendChild(n),P.add(a)}function H({title:t,help:i,body:n,id:s}){return a`<section class="hpf-card" data-card=${s??e}>
${t?a`<div class="hpf-head"><h3>${t}</h3>${i??e}</div>`:e}
<div class="hpf-body">${n}</div>
</section>`}function E({title:t,help:i}){return a`<div class="hpf-sub"><h4>${t}</h4>${i??e}</div>`}function F({label:t,htmlFor:i,help:n,control:s,hint:r,error:o}){return a`<div class="hpf-field">
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{aD as e,l as o,aG as t,ee as s,A as a,ef as i,eg as l,eh as n,E as r,c}from"./houseplan-card-e9vxRZcR.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__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{aD as e,l as o,aG as t,ee as s,A as a,ef as i,eg as l,eh as n,E as r,c}from"./houseplan-card-C2ifrS7A.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${d(h.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{c as a,A as e}from"./houseplan-card-e9vxRZcR.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-aaGs9bci.js";import"./form-kit.styles-Dlhk9WKB.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__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{c as a,A as e}from"./houseplan-card-C2ifrS7A.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-BzQmPxkY.js";import"./form-kit.styles-ORjqyA2O.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__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{ek as e,N as t,a$ as n,L as i,ae as o,ac as r,bT as s,bS as a,a_ as c,aZ as l,b0 as d,el as u,em as f,bV as m,en as g}from"./houseplan-card-e9vxRZcR.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 _(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*b(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 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 _=m.cellCm/o;u+=a(v)*_*_/1e4}return u}catch{return null}}function h(e,t,n=_){const i=b(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{b as cleanFloorAreaSteps,v as representedHaDeviceIds,_ as spaceWallGeometry,S as summaryEntityValue,p as summarySystemValue,h as totalCleanFloorAreaM2};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{ek as e,N as t,a$ as n,L as i,ae as o,ac as r,bV as s,bU as a,a_ as l,aZ as c,b0 as d,el as u,em as f,bX as m,en as g}from"./houseplan-card-C2ifrS7A.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,l="entity"===r?e.registry.entities?.[s]:null,c="device"===r?s:l?.device_id,d="device"===r?a?.area_id:l?.area_id||c&&e.registry.devices?.[c]?.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;c&&e.registry.devices?.[c]&&e.spaceIds.has(f)&&(!n.devices.has(c)||"entity"===r&&n.liveEntities.has(s))&&t.add(c)}return t}function y(e,t){return e?.length?t?.length?g(e,t):e:t}function _(e,t){const n=l(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:c(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*h(l,c,d=_){try{let u=0;for(const f of c){const c=l.spaces.find(e=>String(e?.id)===f.id);if(!c)continue;const m=e(c,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 l=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||a;v=y(v,s(l,m.physicalBodies)),yield}const _=m.cellCm/o;u+=a(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__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{n as t,b as n}from"./zigbee-topology-CXRVgVl2.js";import{aM as e}from"./houseplan-card-e9vxRZcR.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 m(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 g(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,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=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&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(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 m(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,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{n as t,b as n}from"./zigbee-topology-BIziY8_4.js";import{aM as e}from"./houseplan-card-C2ifrS7A.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 f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(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 m(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 g(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(f(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,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=t,g=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 f=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&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),await m(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 m(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{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";try{await import("./houseplan-assets/houseplan-card-e9vxRZcR.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__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";try{await import("./houseplan-assets/houseplan-card-C2ifrS7A.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
+2 -2
View File
@@ -1666,7 +1666,7 @@ export async function prepareGoldenScenario(page, scenario) {
await card.updateComplete;
}
if (scenario.dialog === 'optimize-orphan-references') {
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
// This golden owns orphan-reference copy/layout only. The shared visual
// fixture is intentionally v7, but wall-model migration has its own
@@ -1710,7 +1710,7 @@ export async function prepareGoldenScenario(page, scenario) {
status: 'failed',
reason: 'wall-null',
}));
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
card._alignDialog = {
...card._alignDialog,
preflight: {
+6 -6
View File
@@ -216,7 +216,7 @@ const out = await page.evaluate(async () => {
c._modelCache = null; c._frame = null; c._space = 'g1';
c.requestUpdate(); await c.updateComplete;
c._openAlignDialog();
c._editorRuntime.optimizePlans.open();
await c.updateComplete;
const dlg = c._alignDialog;
o.alignDialogOpens = !!dlg && !!sr().querySelector('hp-dialog .alignmsg');
@@ -229,7 +229,7 @@ const out = await page.evaluate(async () => {
const sent = [];
const base = c.hass.callWS;
c.hass = { ...c.hass, callWS: async (m) => { sent.push(m.type); return base(m); } };
await c._runAlignToGrid();
await c._editorRuntime.optimizePlans.run();
await c.updateComplete;
o.alignDialogClosed = c._alignDialog === null;
o.alignUsedAtomicConfigLayoutWrite = sent.filter((t) => t === 'houseplan/plan/optimize').length === 1;
@@ -249,7 +249,7 @@ const out = await page.evaluate(async () => {
g.openings[0].y - (w0[1] + tt * (w1[1] - w0[1]))) < 1e-9;
// ---- 2b) …and the second run has nothing to do ------------------------
c._openAlignDialog();
c._editorRuntime.optimizePlans.open();
await c.updateComplete;
o.secondRunMovesNothing = c._alignDialog.report.moved === 0;
o.secondRunOffersNoButton = !sr().querySelector('hp-dialog .btn.on');
@@ -272,7 +272,7 @@ const out = await page.evaluate(async () => {
c._layout = {};
c._modelCache = null; c._frame = null; c._space = 'm1';
c.requestUpdate(); await c.updateComplete;
c._openAlignDialog(); await c.updateComplete;
c._editorRuntime.optimizePlans.open(); await c.updateComplete;
const md = c._alignDialog;
o.alignPromiseUsesTheOwnScaleOfEachSpace = !!md && md.cm >= 50 && md.cm < 51;
o.alignPromiseNamesTheSpaceItBelongsTo = !!md && md.where === 'Attic'
@@ -292,11 +292,11 @@ const out = await page.evaluate(async () => {
c._layout = {};
c._modelCache = null; c._frame = null; c._space = 'a1';
c.requestUpdate(); await c.updateComplete;
c._openAlignDialog(); await c.updateComplete;
c._editorRuntime.optimizePlans.open(); await c.updateComplete;
const ad = c._alignDialog;
o.angleOnlyOpeningCounts = !!ad && ad.report.moved === 1 && ad.report.rotated === 1;
o.angleOnlyOpeningOffersTheButton = !!sr().querySelector('hp-dialog .btn.on');
await c._runAlignToGrid();
await c._editorRuntime.optimizePlans.run();
await c.updateComplete;
o.angleOnlyOpeningIsActuallyFixed = c._serverCfg.spaces[0].openings[0].angle === 0;
+4 -4
View File
@@ -50,7 +50,7 @@ const out = await page.evaluate(async (sourceSpace) => {
card.requestUpdate();
await card.updateComplete;
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
const preview = card._alignDialog;
const dialogText = card.renderRoot.querySelector('hp-dialog')?.textContent || '';
@@ -68,9 +68,9 @@ const out = await page.evaluate(async (sourceSpace) => {
await card.updateComplete;
result.cancelDoesNotWrite = sent.length === 0;
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
await card._runAlignToGrid();
await card._editorRuntime.optimizePlans.run();
await card.updateComplete;
const north = card._serverCfg.spaces[0].rooms.find((room) => room.id === 'north-west');
const south = card._serverCfg.spaces[0].rooms.find((room) => room.id === 'south-west');
@@ -85,7 +85,7 @@ const out = await page.evaluate(async (sourceSpace) => {
await card._loadFromServer();
await card.updateComplete;
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
result.reloadIsIdempotent = card._alignDialog.report.wallsStraightened === 0;
card._alignDialog = null;
+4 -4
View File
@@ -64,7 +64,7 @@ const out = await page.evaluate(async () => {
card._modelCache = null; card._frame = null; card._space = 'coincident';
card.requestUpdate(); await card.updateComplete;
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const preview = card._alignDialog;
const previewSpace = preview?.config?.spaces?.[0];
result.previewIsExact = !!preview?.changed && preview.preflight?.ok === true
@@ -77,7 +77,7 @@ const out = await page.evaluate(async () => {
&& JSON.stringify(card._serverCfg) === JSON.stringify(original);
result.reportRendersBothCounters = card.renderRoot.querySelectorAll('hp-dialog .alignmsg').length >= 2;
await card._runAlignToGrid(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
const applied = card._serverCfg.spaces[0];
result.applyUsesOneAtomicWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1;
result.applyKeepsOpeningFields = applied.partitions == null
@@ -103,8 +103,8 @@ const out = await page.evaluate(async () => {
// Apply once more and prove the ordinary shared wall is no longer hidden
// underneath an independent-wall blocker in the remaining Thickness tool.
card._openAlignDialog(); await card.updateComplete;
await card._runAlignToGrid(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
card._setMode('plan'); card._markup = true;
card._modelCache = null; card.requestUpdate(); await card.updateComplete;
card._tool = 'wallthick';
@@ -95,7 +95,7 @@ const out = await page.evaluate(async () => {
card._modelCache = null; card._frame = null; card._space = 'noisy';
card.requestUpdate(); await card.updateComplete;
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const preview = card._alignDialog;
const previewText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.previewOffersInvisibleCleanup = !!preview?.changed
@@ -121,8 +121,8 @@ const out = await page.evaluate(async () => {
result.cancelDoesNotWrite = sent.length === 0
&& JSON.stringify(card._serverCfg) === JSON.stringify(original);
card._openAlignDialog(); await card.updateComplete;
await card._runAlignToGrid(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.applyUsesOneAtomicWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1;
result.applyStoresOnlyCanonicalRoomCoordinates = card._serverCfg.spaces[0].rooms
.every((room) => room.poly.every(([x, y]) => isCanonical(x) && isCanonical(y)));
@@ -135,7 +135,7 @@ const out = await page.evaluate(async () => {
&& !lastToast.includes('0 records maintained');
await Promise.all([card._reloadConfigOnly(true), card._reloadLayoutOnly()]);
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
result.serverEventReloadIsExactNoOp = card._alignDialog?.changed === false
&& card._alignDialog.report.latticeCoordinatesCanonicalized === 0
&& !card.renderRoot.querySelector('hp-dialog .btn.on');
@@ -145,7 +145,7 @@ const out = await page.evaluate(async () => {
card._cfgContentFingerprint = ''; card._layoutContentFingerprint = '';
card._loadOk = false;
await card._loadFromServer();
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
result.coldReloadIsExactNoOp = card._alignDialog?.changed === false
&& card._alignDialog.report.latticeCoordinatesCanonicalized === 0
&& !card.renderRoot.querySelector('hp-dialog .btn.on');
+8 -8
View File
@@ -69,7 +69,7 @@ const out = await page.evaluate(async () => {
configRev: card._cfgRev, layoutRev: card._layoutRev,
canUndo: card._canOptimizeUndo, undoKind: card._undoKind,
};
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const englishText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.englishFailureNamesThreeAndCountsRest = englishText.includes(
'Could not safely verify the geometry of the following spaces: Alpha, beta, Space 3, and 1 more.',
@@ -84,7 +84,7 @@ const out = await page.evaluate(async () => {
&& englishText.includes('Alpha: The wall body did not build (the union came back empty)')
&& englishText.includes('<img id="preflight-injection" src=x>');
result.failureRendersNoApply = !card.renderRoot.querySelector('hp-dialog .btn.on');
await card._runAlignToGrid(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.redPreflightMakesZeroWrites = sent.length === 0;
result.redPreflightPreservesAllState =
JSON.stringify(card._serverCfg) === JSON.stringify(beforeRed.config)
@@ -96,12 +96,12 @@ const out = await page.evaluate(async () => {
// newly red result replaces the dialog and still cannot write.
forceRed = false;
setCandidate('en');
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const checksAfterGreenPreview = checks;
result.greenPreviewOffersApply = !!card.renderRoot.querySelector('hp-dialog .btn.on');
card._alignDialog.config.spaces[0].title = 'Changed after preview';
forceRed = true;
await card._runAlignToGrid(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.changedFingerprintRechecks = checks === checksAfterGreenPreview + 1;
result.changedFingerprintFailsClosed = sent.length === 0
&& !card._alignDialog.preflight.ok
@@ -112,10 +112,10 @@ const out = await page.evaluate(async () => {
forceRed = false;
setCandidate('en');
const callsBeforeGreen = sent.length;
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const checksBeforeApply = checks;
const previewCandidate = clone(card._alignDialog.config);
await card._runAlignToGrid(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.unchangedApplyDoesNotRecheck = checks === checksBeforeApply;
result.greenApplyMakesOneAtomicWrite = sent.length === callsBeforeGreen + 1
&& JSON.stringify(sent.at(-1).config) === JSON.stringify(previewCandidate);
@@ -125,7 +125,7 @@ const out = await page.evaluate(async () => {
// The just-written candidate is now a no-op: it skips the geometry pass and
// retains the established message/absence of Apply.
const checksBeforeNoOp = checks;
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const noOpText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.noOpSkipsPreflight = checks === checksBeforeNoOp
&& card._alignDialog.preflight === null;
@@ -137,7 +137,7 @@ const out = await page.evaluate(async () => {
// The same bounded copy and fallback/count policy is localized in Russian.
forceRed = true;
setCandidate('ru');
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const russianText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.russianFailureHasExactCopy = russianText.includes(
'Не удалось безопасно проверить геометрию следующих пространств: Alpha, beta, Пространство 3 и ещё 1.',
+3 -3
View File
@@ -83,7 +83,7 @@ const out = await page.evaluate(async () => {
card._modelCache = null; card._frame = null; card._space = 'micro';
card.requestUpdate(); await card.updateComplete;
card._openAlignDialog(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const preview = card._alignDialog;
result.previewOffersRoleAwareChange = !!preview?.changed
&& preview.report.canonicalized === 1 && preview.report.wallsMerged === 0
@@ -96,8 +96,8 @@ const out = await page.evaluate(async () => {
result.cancelDoesNotWrite = sent.length === 0
&& JSON.stringify(card._serverCfg) === JSON.stringify(original);
card._openAlignDialog(); await card.updateComplete;
await card._runAlignToGrid(); await card.updateComplete;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
const appliedWalls = card._serverCfg.spaces[0].walls;
result.applyUsesOneAtomicWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1;
result.applyStoresRoleAwareRuns = hasRoleAwareCanonicalRuns(appliedWalls);
+5 -5
View File
@@ -92,7 +92,7 @@ const out = await page.evaluate(async () => {
card.requestUpdate();
await card.updateComplete;
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
const preview = card._alignDialog;
const previewBody = card.renderRoot.querySelector('hp-dialog .body');
@@ -134,9 +134,9 @@ const out = await page.evaluate(async () => {
await card.updateComplete;
result.cancelWritesNothing = calls.length === 0;
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
card._toggleOptimizeLivePositions();
card._editorRuntime.optimizePlans.toggleLivePositions();
await card.updateComplete;
const selectedText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.explicitCleanupRebuildsPreviewWithoutWriting = calls.length === 0
@@ -146,7 +146,7 @@ const out = await page.evaluate(async () => {
&& card.renderRoot.querySelector('.optimize-cleanup')?.getAttribute('aria-pressed') === 'true'
&& selectedText.includes('They are selected for removal.')
&& !selectedText.includes('They will be kept.');
await card._runAlignToGrid();
await card._editorRuntime.optimizePlans.run();
await card.updateComplete;
result.applyUsesExactAtomicEndpoint = calls.filter((type) => type === 'houseplan/plan/optimize').length === 1;
const restored = card._devices.find((device) => device.id === 'orphan');
@@ -154,7 +154,7 @@ const out = await page.evaluate(async () => {
&& restored.icon === 'mdi:washing-machine'
&& !!card.renderRoot.querySelector('.dev[data-id="orphan"]');
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
const noOpText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.remainingOnlyWarningHasNoApply = card._alignDialog?.changed === false
+6 -6
View File
@@ -32,8 +32,8 @@ const out = await page.evaluate(async () => {
{ spaceId: 'bad2', displayName: 'Second', status: 'failed', reason: 'floor-null' },
],
};
card._reportPreflightFailure(preflight);
card._reportPreflightFailure(preflight); // dedup: same fingerprint logs once
card._editorRuntime.optimizePlans.reportPreflightFailure(preflight);
card._editorRuntime.optimizePlans.reportPreflightFailure(preflight); // dedup: same fingerprint logs once
result.devLogOnce = warns.filter((w) => String(w[0]).includes('optimize preflight failed')).length === 1;
const logged = warns.find((w) => String(w[0]).includes('optimize preflight failed'))?.[1];
result.devLogShape = !!logged && logged.kind === 'houseplan-optimize-preflight'
@@ -77,7 +77,7 @@ const out = await page.evaluate(async () => {
let copied = null;
const clipboard = { writeText: async (text) => { copied = text; } };
Object.defineProperty(navigator, 'clipboard', { value: clipboard, configurable: true });
await card._copyPreflightDiagnostics(); await update();
await card._editorRuntime.optimizePlans.copyDiagnostics(); await update();
let block = null;
try { block = JSON.parse(copied); } catch { /* keep null */ }
result.copiedBlock = !!block && block.kind === 'houseplan-optimize-preflight'
@@ -91,7 +91,7 @@ const out = await page.evaluate(async () => {
const saved = card._alignDialog;
card._alignDialog = { ...saved, config: card._serverCfg };
copied = null;
await card._copyPreflightDiagnostics(); await update();
await card._editorRuntime.optimizePlans.copyDiagnostics(); await update();
const savedHash = JSON.parse(copied).failures[0].spaceGeometryFingerprint;
card._alignDialog = saved; await update();
result.fingerprintTracksCandidate = typeof candidateHash === 'string'
@@ -103,7 +103,7 @@ const out = await page.evaluate(async () => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: async () => { throw new Error('denied'); } }, configurable: true,
});
await card._copyPreflightDiagnostics(); await update();
await card._editorRuntime.optimizePlans.copyDiagnostics(); await update();
const pre = root().querySelector('hp-dialog details pre');
result.inlineFallback = !!pre && pre.textContent.includes('houseplan-optimize-preflight');
@@ -115,7 +115,7 @@ const out = await page.evaluate(async () => {
));
await update();
result.fallbackClearedOnClose = card._alignDialog === null
&& card._preflightClipboardFallback === null;
&& card._editorRuntime.optimizePlans.clipboardFallback === null;
console.warn = origWarn;
return result;
+2 -2
View File
@@ -79,7 +79,7 @@ const out = await page.evaluate(async (fixture) => {
result.disabledCapturesNothing = !card._resize.dragging
&& card._geometryHistory.size === 0;
card._openAlignDialog();
card._editorRuntime.optimizePlans.open();
await card.updateComplete;
const previewSpace = card._alignDialog?.config?.spaces?.[0];
result.previewProvesOuterRewrite = card._alignDialog?.changed === true
@@ -89,7 +89,7 @@ const out = await page.evaluate(async (fixture) => {
&& card._alignDialog?.config?.model_version === 10
&& previewSpace?.partitions == null
&& previewSpace?.openings?.every((opening) => wallHostResolves(previewSpace, opening));
await card._runAlignToGrid();
await card._editorRuntime.optimizePlans.run();
await card.updateComplete;
result.optimizeUsesOneWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1;
const optimized = card._serverCfg.spaces[0];
+1 -1
View File
@@ -156,7 +156,7 @@ const result = await page.evaluate(async () => {
&& JSON.stringify(unrelated) === JSON.stringify({
id: 'foreign-unrelated', a: [0.6, 0.5], b: [0.7, 0.5], cm: 15,
});
card._openAlignDialog(); await update();
card._editorRuntime.optimizePlans.open(); await update();
out.acceptedRoomIsOptimizeFixedPoint = card._alignDialog?.report?.partitionsReconciled === 0
&& card._alignDialog?.changed === false;
return out;
+1 -1
View File
@@ -171,7 +171,7 @@ const res = await page.evaluate(async () => {
c._setMode('view'); await c.updateComplete; await sleep(60);
// Use the production preview instead of a partial synthetic report: report
// fields grow with the optimizer contract and the dialog renders all of them.
c._openAlignDialog();
c._editorRuntime.optimizePlans.open();
await c.updateComplete;
out.dAlignOpenBefore = !!c._alignDialog;
c.remove(); await sleep(20);
+1 -1
View File
@@ -12,7 +12,7 @@ const chain = await runWallDrawFinishProfile(page);
const chainHistory = await page.evaluate(async (chainIds) => {
const card = window.__card;
const optimizeNoop = () => {
card._previewAlignDialog(false);
card._editorRuntime.optimizePlans.preview(false);
const changed = card._alignDialog?.changed;
card._alignDialog = null;
return changed === false;
+1 -1
View File
@@ -199,7 +199,7 @@ export async function runWallDrawFinishProfile(page) {
card._activateMarkupTool('select');
const finishMs = performance.now() - started;
const terminal = delta(snapshots(), before);
card._previewAlignDialog(false);
card._editorRuntime.optimizePlans.preview(false);
const optimizeChanged = card._alignDialog?.changed;
card._alignDialog = null;
await card.updateComplete;
+138 -138
View File
@@ -1,127 +1,127 @@
{
"schema": 1,
"fingerprint": "be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6",
"fingerprint": "d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 289449,
"initialViewGzipBytes": 289273,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 290971,
"initialPanelGzipBytes": 290796,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2324,
"lazyFiles": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/de-CjsIUY-d.js",
"houseplan-assets/editor-Dv3uqeSS.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/fr-D2W98VDc.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js",
"houseplan-assets/hp-zigbee-topology-overlay-C5T09STo.js",
"houseplan-assets/iso-scene-render-3HN3TF4R.js",
"houseplan-assets/live-interaction-runtime-kRE37k2_.js",
"houseplan-assets/pdf-export-DKVHRlMO.js",
"houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js",
"houseplan-assets/zigbee-topology-runtime-C4WynS64.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/de-C_-FOd3R.js",
"houseplan-assets/editor-CuOqOWa_.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/fr-BubWZSkS.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js",
"houseplan-assets/hp-zigbee-topology-overlay-DQp1tIHa.js",
"houseplan-assets/iso-scene-render-DpO3ezuP.js",
"houseplan-assets/live-interaction-runtime-CPQNHLqJ.js",
"houseplan-assets/pdf-export-CFT_MzmM.js",
"houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js",
"houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js"
],
"lazyGzipBytes": 460916,
"lazyGzipBytes": 461471,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/editor-Dv3uqeSS.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/editor-CuOqOWa_.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js"
],
"lazyEditorGzipBytes": 244420,
"lazyEditorGzipBytes": 244976,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js"
],
"lazyOnboardingGzipBytes": 34521,
"lazyOnboardingGzipBytes": 34523,
"lazyLocaleFiles": [
"houseplan-assets/de-CjsIUY-d.js",
"houseplan-assets/fr-D2W98VDc.js"
"houseplan-assets/de-C_-FOd3R.js",
"houseplan-assets/fr-BubWZSkS.js"
],
"lazyLocaleGzipBytes": 55255,
"lazyLocaleGzipBytes": 55257,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-3HN3TF4R.js"
"houseplan-assets/iso-scene-render-DpO3ezuP.js"
],
"lazyIsometricGzipBytes": 17640,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js"
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js"
],
"lazyFurnitureArtGzipBytes": 16940,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/pdf-export-DKVHRlMO.js"
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/pdf-export-CFT_MzmM.js"
],
"lazyPdfGzipBytes": 126827,
"lazyPdfGzipBytes": 126826,
"files": [
{
"path": "houseplan-assets/backdrop-pick-YbdnEEmM.js",
"sha256": "07acc6fc3e12ba2fe3e1b07e19a3f57308a0834a03db3c01bdba22ea43b9a7fd",
"path": "houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"sha256": "37be8583306fa604aed0f7f79f7a6c3eb8e373de8e5d476fb4593881b7954f93",
"rawBytes": 76524,
"gzipBytes": 24571,
"gzipBytes": 24572,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CjsIUY-d.js",
"sha256": "25fa2d8f19df083532e8f022d86c21d290f3e79badaf7b4d058158c50a02286c",
"path": "houseplan-assets/de-C_-FOd3R.js",
"sha256": "033535ad61de5d4453c3b3729cf2afb69ed996267cbb83ebd2111b0ce6cad359",
"rawBytes": 92247,
"gzipBytes": 27864,
"gzipBytes": 27865,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Dv3uqeSS.js",
"sha256": "b1cdd26dec11ae7d904a675fbc27964757819886d1034ab63d2c9d91b42e3655",
"path": "houseplan-assets/editor-CuOqOWa_.js",
"sha256": "350885825a2046833dcef868485cae1efe99389d088fd502bd166d1ecc75b2a7",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1581,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"sha256": "aee7494545b43e2c932163c572b19483d9ac35c022daf437bcfd3aa21589aa07",
"path": "houseplan-assets/form-kit.styles-ORjqyA2O.js",
"sha256": "c6899e84bdfc9d83f635e63317260c6e3e0ae85104d966e0e297d56abcf45d91",
"rawBytes": 25600,
"gzipBytes": 5055,
"gzipBytes": 5056,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-D2W98VDc.js",
"sha256": "12e70d1138d31d87f8c7f25d750bef3ecb7c84e23925845c101f0af4298208c0",
"path": "houseplan-assets/fr-BubWZSkS.js",
"sha256": "6243e53b2d25b50f23e7b8544cf24ae2634f3d985d0ebb05ad881628121693f4",
"rawBytes": 94579,
"gzipBytes": 27391,
"gzipBytes": 27392,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"sha256": "63e67cda0c59f37fee93410ae4193ae79d730b83293eea331edefaf63e280d11",
"path": "houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"sha256": "aac82444a5129c8991694a9a5421a63fc23479d41ed2cf44be2bba9cdb8bac38",
"rawBytes": 52035,
"gzipBytes": 16940,
"isEntry": false,
@@ -129,184 +129,184 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-e9vxRZcR.js",
"sha256": "98c44bf6f915d6121132c133cc12b2e27ea344f1c8203e7af54741f317aa1a5e",
"rawBytes": 1022206,
"gzipBytes": 288647,
"path": "houseplan-assets/houseplan-card-C2ifrS7A.js",
"sha256": "6e2ecbe962122cfa7599c75e5ca8815d27c98b754cc437b7628b4ddafef10d9f",
"rawBytes": 1021428,
"gzipBytes": 288472,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CjsIUY-d.js",
"houseplan-assets/editor-Dv3uqeSS.js",
"houseplan-assets/fr-D2W98VDc.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js",
"houseplan-assets/hp-zigbee-topology-overlay-C5T09STo.js",
"houseplan-assets/iso-scene-render-3HN3TF4R.js",
"houseplan-assets/live-interaction-runtime-kRE37k2_.js",
"houseplan-assets/pdf-export-DKVHRlMO.js",
"houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js"
"houseplan-assets/de-C_-FOd3R.js",
"houseplan-assets/editor-CuOqOWa_.js",
"houseplan-assets/fr-BubWZSkS.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js",
"houseplan-assets/hp-zigbee-topology-overlay-DQp1tIHa.js",
"houseplan-assets/iso-scene-render-DpO3ezuP.js",
"houseplan-assets/live-interaction-runtime-CPQNHLqJ.js",
"houseplan-assets/pdf-export-CFT_MzmM.js",
"houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime--nYeDmTG.js",
"sha256": "f476a861b5a2d77cd2ecea724c57d4e9e6dce6fa161eb18cc9479d2ec9752641",
"rawBytes": 678218,
"gzipBytes": 188846,
"path": "houseplan-assets/houseplan-editor-runtime-B5TSesBb.js",
"sha256": "7bad833f2fd60af11c8411e56b97431c71ee3e4fff685de3019a9ac14e5bc1ba",
"rawBytes": 678531,
"gzipBytes": 189399,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-C4WynS64.js"
"houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-zgKhvtTD.js",
"sha256": "5613ece2554c3ee9afc3e71dce63370d3e0bbeab9489e297ffaee74f33670e6f",
"path": "houseplan-assets/houseplan-onboarding-runtime-BgE55y_R.js",
"sha256": "1a16adf7c3ab18db3a9773930103843470acdeb2fde3870110c20a069cd28876",
"rawBytes": 15986,
"gzipBytes": 4895,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-YbdnEEmM.js",
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/backdrop-pick-CAEOSiNJ.js",
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-C5T09STo.js",
"sha256": "d836d918b89ee5685f7d2d10b194cf17feb7ca956c2112c78c4d62db7042ef24",
"path": "houseplan-assets/hp-zigbee-topology-overlay-DQp1tIHa.js",
"sha256": "13cf97cdc63e8b66108521655d6e4585452fcf02ce9f4fc0b0bf19c13cc6f099",
"rawBytes": 10759,
"gzipBytes": 3686,
"gzipBytes": 3685,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js",
"houseplan-assets/zigbee-topology-runtime-C4WynS64.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js",
"houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-3HN3TF4R.js",
"sha256": "0c1137699d170125b0b3a6a7dd5177a06e270d74e8b3020711c100b4c59bbe0d",
"path": "houseplan-assets/iso-scene-render-DpO3ezuP.js",
"sha256": "c979378b329bca0dbd23ddbae6ca2cad763bb5754959e9b98f395197d915a8b5",
"rawBytes": 51983,
"gzipBytes": 17640,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-kRE37k2_.js",
"sha256": "e0537b8b7d78df3e70208e5601b95b538509265184027a62b559c43626372385",
"path": "houseplan-assets/live-interaction-runtime-CPQNHLqJ.js",
"sha256": "10e83f4eb0bdf1e4b0b0a4c9e377ade059bf5169488fa360821625a4df4bc91f",
"rawBytes": 8216,
"gzipBytes": 3203,
"gzipBytes": 3206,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-DKVHRlMO.js",
"sha256": "fef91b049f58622de45132f31b3ab7f606df5db8eee3fc68d3809b2c5ece7e5d",
"path": "houseplan-assets/pdf-export-CFT_MzmM.js",
"sha256": "03ee0fc3e88bc84e94c1a51fadc2e65a17635ba53ed875f1cdb794c3eb989106",
"rawBytes": 236264,
"gzipBytes": 109887,
"gzipBytes": 109886,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-YwpfkuFp.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/furniture-plan-art.generated-Ci984yNn.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-Bj45k8M3.js",
"sha256": "1463acf70394eb7c8fc09b8182b227648b00919bb2fc5ad6a6ae189b1a051a2e",
"path": "houseplan-assets/summary-panel-editor-11x1Zz63.js",
"sha256": "2d4be3e90f3c0329c33b6465d57c82fc2ba93e30097838cd042c69f0cc0f08be",
"rawBytes": 14862,
"gzipBytes": 3423,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js"
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-3K7HlQXp.js",
"sha256": "246a3ee30014c9096527044921c104b76cef9555ebd1b2fbfcaeea54daf010cc",
"path": "houseplan-assets/summary-panel-metrics-BwUdppyx.js",
"sha256": "5dbb467356ea155e5d4dee21c562cbfdf0a763b93d728f8227f7731b91837068",
"rawBytes": 2611,
"gzipBytes": 1306,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-aaGs9bci.js",
"sha256": "527c40315a7cbbfa84289d6c8360688df121f368bf2609488e0b3b527ed2988a",
"path": "houseplan-assets/summary-panel-runtime-loaded-BzQmPxkY.js",
"sha256": "6e0e9808f1da372aef041d74e46c91e29d2d5fd98a9fbe24ee54debe58c06ee3",
"rawBytes": 72637,
"gzipBytes": 20166,
"gzipBytes": 20163,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Dlhk9WKB.js",
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/form-kit.styles-ORjqyA2O.js",
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-Bj45k8M3.js",
"houseplan-assets/summary-panel-metrics-3K7HlQXp.js"
"houseplan-assets/summary-panel-editor-11x1Zz63.js",
"houseplan-assets/summary-panel-metrics-BwUdppyx.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-CXRVgVl2.js",
"sha256": "a68916a24db8c6acc389707ad40f95d8963d4521e40c09e6fa32b6430584569a",
"path": "houseplan-assets/zigbee-topology-BIziY8_4.js",
"sha256": "be1428736b1fcd653bec4a452d4895d8cfea015cc143b8560f5c6065c8b6ffea",
"rawBytes": 18962,
"gzipBytes": 7425,
"gzipBytes": 7428,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-C4WynS64.js",
"sha256": "598d78eb03a6b75e7bf9ce8ac7fdbc7ee514d6c7f056e8651309c541313ced0e",
"path": "houseplan-assets/zigbee-topology-runtime-Sxlr6W0z.js",
"sha256": "af36ce4a7b2e9b6992e2c74a19e63f197a1960317fad3bb8f193d93ccccb84b8",
"rawBytes": 3942,
"gzipBytes": 1764,
"gzipBytes": 1763,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js",
"houseplan-assets/zigbee-topology-CXRVgVl2.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js",
"houseplan-assets/zigbee-topology-BIziY8_4.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "ff5753ceb4dce57c6f1701baad303c37770735cb53b2a61c14e1225e07ac898b",
"sha256": "9a30c8176149f7b6b78431e5f876dd8ab3f86676bf31ecfbd8952821812a46ed",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 801,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "90fb829ea2a8310205a7bc7c8d5d57bd38eedb0d62c961eed13d3bc3119b011a",
"sha256": "e712ffa4a818f355510e4eee5ed9438c6f89ac60797f0838bba4b2298cb35d24",
"rawBytes": 6165,
"gzipBytes": 2324,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-e9vxRZcR.js"
"houseplan-assets/houseplan-card-C2ifrS7A.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{A as e,c as a,s as t,l as i,d as n,ap as s,h as r,j as o,g as l,dH as c,dI as p,dJ as d,cL as h,dK as u,cK as m,dL as g,cQ as _,cP as b,dM as f,bs as v,br as w,bt as y,av as k,aC as $,aD as x,dN as S}from"./houseplan-card-e9vxRZcR.js";import{c as C,f as M,C as L}from"./form-kit.styles-Dlhk9WKB.js";const z=C+M(L,{withSwitch:!1}),P=new WeakSet;let D=null;function T(e){const a=e.renderRoot;if(!a||P.has(a))return;const t=a.ownerDocument??globalThis.document,i=t?.defaultView?.CSSStyleSheet;if(i&&"adoptedStyleSheets"in a)return D||(D=new i,D.replaceSync(z)),a.adoptedStyleSheets=[...a.adoptedStyleSheets,D],void P.add(a);if(a.querySelector("style[data-hp-form-kit]"))return void P.add(a);const n=t.createElement("style");n.dataset.hpFormKit="true",n.textContent=z,a.appendChild(n),P.add(a)}function H({title:t,help:i,body:n,id:s}){return a`<section class="hpf-card" data-card=${s??e}>
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{A as e,c as a,s as t,l as i,d as n,ap as s,h as r,j as o,g as l,dH as c,dI as p,dJ as d,cN as h,dK as u,cM as m,dL as g,cS as _,cR as b,dM as f,bt as v,bs as w,bu as y,av as k,aC as $,aD as x,dN as S}from"./houseplan-card-C2ifrS7A.js";import{c as C,f as M,C as L}from"./form-kit.styles-ORjqyA2O.js";const z=C+M(L,{withSwitch:!1}),P=new WeakSet;let D=null;function T(e){const a=e.renderRoot;if(!a||P.has(a))return;const t=a.ownerDocument??globalThis.document,i=t?.defaultView?.CSSStyleSheet;if(i&&"adoptedStyleSheets"in a)return D||(D=new i,D.replaceSync(z)),a.adoptedStyleSheets=[...a.adoptedStyleSheets,D],void P.add(a);if(a.querySelector("style[data-hp-form-kit]"))return void P.add(a);const n=t.createElement("style");n.dataset.hpFormKit="true",n.textContent=z,a.appendChild(n),P.add(a)}function H({title:t,help:i,body:n,id:s}){return a`<section class="hpf-card" data-card=${s??e}>
${t?a`<div class="hpf-head"><h3>${t}</h3>${i??e}</div>`:e}
<div class="hpf-body">${n}</div>
</section>`}function E({title:t,help:i}){return a`<div class="hpf-sub"><h4>${t}</h4>${i??e}</div>`}function F({label:t,htmlFor:i,help:n,control:s,hint:r,error:o}){return a`<div class="hpf-field">
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{aD as e,l as o,aG as t,ee as s,A as a,ef as i,eg as l,eh as n,E as r,c}from"./houseplan-card-e9vxRZcR.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__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{aD as e,l as o,aG as t,ee as s,A as a,ef as i,eg as l,eh as n,E as r,c}from"./houseplan-card-C2ifrS7A.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${d(h.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
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
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{c as a,A as e}from"./houseplan-card-e9vxRZcR.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-aaGs9bci.js";import"./form-kit.styles-Dlhk9WKB.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__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{c as a,A as e}from"./houseplan-card-C2ifrS7A.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-BzQmPxkY.js";import"./form-kit.styles-ORjqyA2O.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__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{ek as e,N as t,a$ as n,L as i,ae as o,ac as r,bT as s,bS as a,a_ as c,aZ as l,b0 as d,el as u,em as f,bV as m,en as g}from"./houseplan-card-e9vxRZcR.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 _(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*b(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 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 _=m.cellCm/o;u+=a(v)*_*_/1e4}return u}catch{return null}}function h(e,t,n=_){const i=b(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{b as cleanFloorAreaSteps,v as representedHaDeviceIds,_ as spaceWallGeometry,S as summaryEntityValue,p as summarySystemValue,h as totalCleanFloorAreaM2};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{ek as e,N as t,a$ as n,L as i,ae as o,ac as r,bV as s,bU as a,a_ as l,aZ as c,b0 as d,el as u,em as f,bX as m,en as g}from"./houseplan-card-C2ifrS7A.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,l="entity"===r?e.registry.entities?.[s]:null,c="device"===r?s:l?.device_id,d="device"===r?a?.area_id:l?.area_id||c&&e.registry.devices?.[c]?.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;c&&e.registry.devices?.[c]&&e.spaceIds.has(f)&&(!n.devices.has(c)||"entity"===r&&n.liveEntities.has(s))&&t.add(c)}return t}function y(e,t){return e?.length?t?.length?g(e,t):e:t}function _(e,t){const n=l(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:c(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*h(l,c,d=_){try{let u=0;for(const f of c){const c=l.spaces.find(e=>String(e?.id)===f.id);if(!c)continue;const m=e(c,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 l=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||a;v=y(v,s(l,m.physicalBodies)),yield}const _=m.cellCm/o;u+=a(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};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";import{n as t,b as n}from"./zigbee-topology-CXRVgVl2.js";import{aM as e}from"./houseplan-card-e9vxRZcR.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 m(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 g(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,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=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&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(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 m(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,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";import{n as t,b as n}from"./zigbee-topology-BIziY8_4.js";import{aM as e}from"./houseplan-card-C2ifrS7A.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 f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(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 m(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 g(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(f(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,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=t,g=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 f=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&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),await m(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 m(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{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="be980214e65258ad0c8ec0480b3b70c46a1e6f817bba2cda8d2f16c104021db6";try{await import("./houseplan-assets/houseplan-card-e9vxRZcR.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__="d85694de8aee015f4930ff31f4c2022c14e45b0ad38f1dd1e723229f27f1e7b1";try{await import("./houseplan-assets/houseplan-card-C2ifrS7A.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
+7 -1
View File
@@ -466,8 +466,14 @@ export const LAZY_FURNITURE_ART_GZIP_CEILING = 17_900;
* inline-ошибки маркера и единая повторная валидация живут в lazy
* editor-графе. Новый центр оставляет 1 044 Б сверху и 956 Б до
* нижней границы полосы.
* - #642: 245 400 → 246 000 (замер 244 975, база 244 420). Диалог
* «Оптимизировать планы» уехал из рантайма в свой модуль за узкий порт:
* сырых байт стало меньше на 465, а gzip вырос на 555 — обёртка класса и
* адаптер порта из 18 членов уникальны, а сжатие соседних `this.host._t(`
* в рантайме их не покрывает. Старый центр оставлял 425 Б сверху, меньше
* порога шума #593; новый — 1 025 Б сверху и 975 Б до нижней границы.
*/
export const LAZY_EDITOR_GZIP_CEILING = 245_400;
export const LAZY_EDITOR_GZIP_CEILING = 246_000;
export const LAZY_GRAPH_CEILING_BAND = 2_000;
/**
+6 -6
View File
@@ -1,8 +1,8 @@
{
"delegates": 159,
"portMembers": 350,
"hostRefs": 4948,
"portPrivates": 96,
"harnessPrivates": 107,
"bundleBytes": 2500387
"delegates": 154,
"portMembers": 348,
"hostRefs": 4869,
"portPrivates": 94,
"harnessPrivates": 101,
"bundleBytes": 2499182
}
+79 -33
View File
@@ -5254,13 +5254,18 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'optimize-preflight-bypassed',
guard: 'node demo/smoke_optimize_geometry_preflight.mjs',
// #642: диалог вынесен в свой модуль, гвард — его юнит.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="red preflight is a hard write barrier" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'a red preview must remain a hard write barrier even if a caller invokes the '
+ 'private Apply method directly instead of clicking the deliberately absent button (#199)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' if (!d || d.busy || !this._serverCfg || !d.changed || !d.preflight?.ok) return;',
replace: ' if (!d || d.busy || !this._serverCfg || !d.changed) return;',
file: 'src/optimize-plans-dialog.ts',
find: ' if (!d || d.busy || !this.port.config() || !d.changed || !d.preflight?.ok) return;',
// `!d.preflight` остаётся ради сужения типа: иначе мутант не компилируется
// и ловится компилятором, а не тестом.
replace: ' if (!d || d.busy || !this.port.config() || !d.changed || !d.preflight) return;',
}],
},
{
@@ -5291,11 +5296,14 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'optimize-preflight-renders-apply-on-failure',
guard: 'node demo/smoke_optimize_geometry_preflight.mjs',
// #642: разметка диалога — исполнением в юните модуля.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="failed preflight names each reason and offers no Apply" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'the failure state is not a dismissible warning: rendering Apply invites a person '
+ 'to treat an unsafe whole-plan candidate as an accepted risk (#199)',
patches: [{
file: 'src/houseplan-card.ts',
file: 'src/optimize-plans-dialog.ts',
find: ' ${!d.changed || !d.preflight?.ok ? nothing : html`',
replace: ' ${!d.changed ? nothing : html`',
}],
@@ -5402,15 +5410,18 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'near-axis-optimize-confirmation-bypassed',
guard: 'node demo/smoke_near_axis_optimize.mjs',
// #642: открытие — метод модуля диалога; гвард — его юнит.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="preview never writes" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'opening the Optimize preview or cancelling it must never persist a lossy repair (#290)',
patches: [{
file: 'src/houseplan-editor-runtime.ts',
find: 'public _openAlignDialog = (): void => this._previewAlignDialog(false);',
replace: 'public _openAlignDialog = (): void => {\n'
+ ' this._previewAlignDialog(false);\n'
+ ' void this._runAlignToGrid();\n'
+ ' };',
file: 'src/optimize-plans-dialog.ts',
find: ' public open(): void { this.preview(false); }',
replace: ' public open(): void {\n'
+ ' this.preview(false);\n'
+ ' void this.run();\n'
+ ' }',
}],
},
{
@@ -6622,12 +6633,14 @@ const MUTANT_DEFINITIONS = [
{
id: 'preflight-reason-lost-in-dialog',
// #295: диалог обязан называть причину отказа по каждому пространству.
// #642: гвард — юнит модуля диалога, разметка исполнением.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/smoke_preflight_diagnostics.mjs',
+ '&& node --test --test-name-pattern="failed preflight names each reason and offers no Apply" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'отказ preflight без причины недиагностируем — ровно исходный дефект #295',
patches: [{
file: 'src/houseplan-editor-runtime.ts',
find: " ${failure.displayName}: ${this.host._t(`gs.preflight_reason_${failure.reason}` as I18nKey)}",
file: 'src/optimize-plans-dialog.ts',
find: " ${failure.displayName}: ${this.port.t(`gs.preflight_reason_${failure.reason}` as I18nKey)}",
replace: " ${failure.displayName}",
}],
},
@@ -6636,15 +6649,16 @@ const MUTANT_DEFINITIONS = [
// CODE-REVIEW-295-r1 M1: хэш геометрии обязан браться из кандидата,
// который проверял preflight, а не из сохранённого конфига — иначе блок
// повторяет то, что и так даст экспорт пространства.
// #642: сборка блока — чистая функция, сохранённый конфиг ей не виден;
// подмена кандидата сохранённым конфигом возможна только у вызова.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/smoke_preflight_diagnostics.mjs',
+ '&& node --test --test-name-pattern="hashes the candidate, not the saved config" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'диагностика с хэшем сохранённой геометрии не несёт ничего сверх экспорта (AC4)',
patches: [{
// This method lives on HouseplanEditorRuntime after the editor split;
// the saved config belongs to the typed host, not to the runtime itself.
file: 'src/houseplan-editor-runtime.ts',
find: " const spacesById = new Map(((candidate as any)?.spaces || [])",
replace: " const spacesById = new Map(((this.host._serverCfg as any)?.spaces || [])",
file: 'src/optimize-plans-dialog.ts',
find: " this.diagnostics(preflight, dialog.config ?? null), null, 2,",
replace: " this.diagnostics(preflight, this.port.config()), null, 2,",
}],
},
{
@@ -6679,39 +6693,71 @@ const MUTANT_DEFINITIONS = [
id: 'preflight-fallback-survives-dialog-close',
// CODE-REVIEW-295-r1 M2: инлайн-фолбэк живёт одно показание диалога;
// переживший закрытие блок подсунет в отчёт диагностику чужого отказа.
// #642: защита — ключ WeakMap по объекту диалога. Мутант подменяет его
// общим ключом (сам контроллер), и фолбэк переживает закрытие.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/smoke_preflight_diagnostics.mjs',
+ '&& node --test --test-name-pattern="belongs to one dialog showing" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'застрявший фолбэк отдаёт в баг-репорт JSON предыдущего отказа, не текущего (AC4)',
patches: [{
file: 'src/houseplan-card.ts',
find: "dismiss-on-scrim @hp-close=${() => { this._alignDialog = null; this._preflightClipboardFallback = null; }}>",
replace: "dismiss-on-scrim @hp-close=${() => { this._alignDialog = null; }}>",
file: 'src/optimize-plans-dialog.ts',
find: " this.fallbacks.set(dialog, text);",
replace: " this.fallbacks.set(this, text);",
}, {
file: 'src/optimize-plans-dialog.ts',
find: " return dialog ? this.fallbacks.get(dialog) ?? null : null;",
replace: " return dialog ? this.fallbacks.get(this) ?? null : null;",
}],
},
{
id: 'preflight-diagnostics-without-reason',
// #295: копируемый блок без reason бесполезен для отчёта об ошибке.
// #642: блок собирает чистая функция модуля диалога.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/smoke_preflight_diagnostics.mjs',
+ '&& node --test --test-name-pattern="preflightDiagnostics: candidate hash, reason" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'диагностический блок обязан нести reason каждого отказа',
patches: [{
file: 'src/houseplan-card.ts',
find: " reason: failure.reason,",
replace: " reason: undefined,",
file: 'src/optimize-plans-dialog.ts',
find: " reason: failure.reason,",
replace: " reason: undefined,",
}],
},
{
id: 'preflight-dev-log-disabled',
// #295: dev-лог — второй канал диагностики, его потерю обязан ловить смок.
// #642: dev-лог и его дедуп живут в модуле диалога.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/smoke_preflight_diagnostics.mjs',
+ '&& node --test --test-name-pattern="logs one structured record per distinct fingerprint" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'структурированная запись отказа в консоли — часть контракта диагностики #295',
patches: [{
file: 'src/houseplan-card.ts',
find: " console.warn('[houseplan] optimize preflight failed', this._preflightDiagnostics(preflight, candidate));",
file: 'src/optimize-plans-dialog.ts',
find: " console.warn('[houseplan] optimize preflight failed', this.diagnostics(preflight, candidate));",
replace: " void preflight; void candidate;",
}],
},
{
id: 'optimize-dialog-imports-host-port',
// #642 AC2: смысл выноса — узкий порт. Модуль, который снова тянет
// широкий порт редактора, возвращает связность, ради снижения которой
// и выносился, а юнит должен это увидеть даже для `import type`.
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="the dialog port is narrow" '
+ 'test/optimize-plans-dialog.test.mjs',
because: 'диалог, получивший HouseplanEditorHostPort, снова связан с 350 членами карточки (#642)',
patches: [{
file: 'src/optimize-plans-dialog.ts',
find: "import { html, nothing, type TemplateResult } from 'lit';\n",
replace: "import { html, nothing, type TemplateResult } from 'lit';\n"
+ "import type { HouseplanEditorHostPort } from './houseplan-editor-runtime';\n",
}, {
file: 'src/optimize-plans-dialog.ts',
find: " private reportedFingerprint: string | null = null;\n",
replace: " private reportedFingerprint: string | null = null;\n"
+ " public host: HouseplanEditorHostPort | null = null;\n",
}],
},
{
id: 'visual-mitre-limit-back-to-4',
// #309: порог визуального среза возвращается к классическим 4·h — шип на
+1 -1
View File
@@ -245,7 +245,7 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
</div>` : nothing}
${subsection({ title: t('gs.grid_group'), help: this._help('gs.grid_group.help') })}
<div class="hpf-actions">
<button class="btn" @click=${() => this._openAlignDialog()}>
<button class="btn" @click=${() => this.optimizePlans.open()}>
<ha-icon icon="mdi:broom"></ha-icon>${t('gs.align_all')}
</button>
${host._canOptimizeUndo && host._undoKind !== 'import' ? html`
+4 -66
View File
@@ -12,6 +12,7 @@ import { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js';
import './hp-dialog';
import type { HpDialog } from './hp-dialog';
import type { OptimizePlansDialogState } from './optimize-plans-dialog';
import './hp-confirm';
import {
HpConfirmController,
@@ -137,7 +138,6 @@ import {
iconUnit, iconCqw, itemOf, snapPt, MIN_ZOOM, PAN_SLACK, CANVAS_LIMIT, GRID_PITCH, GRID_STEP_N,
clampCanvasR, clampCanvasN, type ContentItem, type Rect,
} from './space-geometry';
import { type OptimizeReport } from './plan-optimizer';
import { resolveZeroWalls, zeroWallStyleOf } from './zero-walls';
import {
geometryOpenings, geometryPartitionOpeningCuts, geometryRoomOpeningInputs,
@@ -2003,9 +2003,6 @@ export class HouseplanCard extends LitElement {
private _onboardingShown = false; // the auto space dialog is shown once per session
private _rulesDialog: { rules: IconRule[]; test: string; busy: boolean } | null = null;
/** Optimization preview plus the exact pair, so commit cannot differ from it. */
/** #295: diagnostics text shown inline when the clipboard is unavailable. */
private _preflightClipboardFallback: string | null = null;
/** #295: integration version from houseplan/config/get; null on old backends. */
private _haIntegrationVersion: string | null = null;
/** #462: eager full-card recovery; the static space card never owns one. */
@@ -2026,18 +2023,7 @@ export class HouseplanCard extends LitElement {
private _haRadarStage1Api: number | null = null;
private readonly _radarLive = new RadarLiveController(this);
private _decorAssetSyncToken = 0;
private _alignDialog: {
report: OptimizeReport; config: any; layout: Record<string, any>;
preflight: OptimizeGeometryPreflightResult | null;
/** the promised maximum, in centimetres, ALREADY rounded up (AUD-158B1-01) */
cm: number;
/** the space that maximum belongs to, named only when there are several */
where: string;
changed: boolean;
busy: boolean;
/** False by default; true only after the secondary preview action. */
removeLiveMissingPositions: boolean;
} | null = null;
private _alignDialog: OptimizePlansDialogState | null = null;
private _settingsDialog: {
colors: FillColors; glowRadius: number; glowRadiusInput: string; bgColor: string | null;
@@ -2500,7 +2486,6 @@ export class HouseplanCard extends LitElement {
_pdfDialog: { state: true },
_supportDialog: { state: true },
_alignDialog: { state: true },
_preflightClipboardFallback: { state: true },
_backupExportDialog: { state: true },
_backupImportDialog: { state: true },
_importDialog: { state: true },
@@ -2789,7 +2774,7 @@ export class HouseplanCard extends LitElement {
if (this._openingInfo) { this._openingInfo = null; return; }
if (this._infoCard) { this._closeInfoCard(); return; }
if (this._rulesDialog) { this._rulesDialog = null; return; }
if (this._alignDialog) { this._alignDialog = null; this._preflightClipboardFallback = null; return; }
if (this._alignDialog) { this._alignDialog = null; return; }
if (this._backupImportDialog) { this._backupImportDialog = null; return; }
if (this._backupExportDialog) { this._backupExportDialog = null; return; }
if (this._pdfDialog) { this._pdfDialog = false; return; }
@@ -7393,7 +7378,6 @@ export class HouseplanCard extends LitElement {
this._closeInfoCard();
this._rulesDialog = null;
this._alignDialog = null;
this._preflightClipboardFallback = null;
this._backupImportDialog = null;
this._backupExportDialog = null;
this._pdfDialog = false;
@@ -10197,24 +10181,6 @@ export class HouseplanCard extends LitElement {
});
}
/**
* Preview whole-plan maintenance. Nothing is written here: the pure run
* produces both the report and the exact config/layout pair to commit.
*/
/** #295: dev-log once per distinct failing preflight, not once per render. */
private _reportedPreflightFingerprint: string | null = null;
private _reportPreflightFailure(
preflight: OptimizeGeometryPreflightResult,
candidate: ServerConfig | null,
): void {
return this._editorRuntimeOrThrow()._reportPreflightFailure(preflight, candidate);
}
private async _copyPreflightDiagnostics(): Promise<void> {
return this._editorRuntimeOrThrow()._copyPreflightDiagnostics();
}
private _checkOptimizeGeometry(config: ServerConfig): OptimizeGeometryPreflightResult {
return this._editorRuntimeOrThrow()._checkOptimizeGeometryImpl(config);
}
@@ -10230,26 +10196,6 @@ export class HouseplanCard extends LitElement {
return this._editorRuntimeOrThrow()._checkSpacePhysicalGeometryImpl(config, spaceId, captureWallGeometry);
}
private _previewAlignDialog(removeLiveMissingPositions: boolean): void {
return this._editorRuntimeOrThrow()._previewAlignDialog(removeLiveMissingPositions);
}
private _openAlignDialog = (): void => {
return this._editorRuntimeOrThrow()._openAlignDialog();
}
private _toggleOptimizeLivePositions = (): void => {
return this._editorRuntimeOrThrow()._toggleOptimizeLivePositions();
}
/**
* The backend persists an intent before either store changes, then keeps a
* one-deep snapshot that remains undoable until the next plan edit.
*/
private async _runAlignToGrid(): Promise<void> {
return this._editorRuntimeOrThrow()._runAlignToGrid();
}
/** Prevent a double click from sending two restores of the same snapshot. */
private _optimizeUndoBusy = false;
@@ -10533,14 +10479,6 @@ export class HouseplanCard extends LitElement {
});
}
/**
* The confirmation separates geometry movement from lossless maintenance,
* and promises the one-deep undo before either store is changed.
*/
private _renderAlignDialog(): TemplateResult {
return this._editorRuntimeOrThrow()._renderAlignDialog();
}
private _renderSettingsDialog(): TemplateResult {
return this._editorRuntimeOrThrow()._renderSettingsDialog();
}
@@ -11305,7 +11243,7 @@ export class HouseplanCard extends LitElement {
${this._settingsDialog ? this._editorRuntime ? this._renderSettingsDialog() : nothing : nothing}
${this._supportDialog ? this._editorRuntime ? this._renderSupportDialog() : nothing : nothing}
${this._pdfDialog ? this._renderPdfDialog() : nothing}
${this._alignDialog ? this._editorRuntime ? this._renderAlignDialog() : nothing : nothing}
${this._alignDialog && this._editorRuntime ? this._editorRuntime.optimizePlans.render() : nothing}
${this._backupExportDialog ? this._editorRuntime ? this._renderBackupExportDialog() : nothing : nothing}
${this._backupImportDialog ? this._editorRuntime ? this._renderBackupImportDialog() : nothing : nothing}
${this._importDialog ? this._onboardingRuntime ? this._renderImportDialog() : nothing : nothing}
+26 -388
View File
@@ -148,6 +148,7 @@ import {
import { finishMarkerDialogClose } from './marker-dialog-close';
import { renderRadarSection } from './editors/radar-section';
import { radarDiscardRequest, RadarSetupController } from './radar-setup';
import { OptimizePlansDialog, type OptimizePlansDialogState } from './optimize-plans-dialog';
import {
COLUMN_MAX_CM, canonicalColumnAngle, clampColumnCm, columnBody, floorMinusBodies, geometryArea,
partitionBody, pointInPhysicalBody, sameColumnPlacement, type PartitionOpeningCut,
@@ -182,7 +183,6 @@ import {
spaceModels, iconCqw, gridLevels, CANVAS_LIMIT, SANE_LIMIT, GRID_PITCH, GRID_STEP_N,
PLAN_SCALE_MIN, PLAN_SCALE_MAX, clampCanvasR, clampCanvasN, type Rect,
} from './space-geometry';
import { optimizePlans, type OptimizeReport } from './plan-optimizer';
import {
WALL_SEGMENT_MODEL_VERSION,
adoptWallSegmentModelCandidateInPlace, commitWallSegmentModel,
@@ -199,7 +199,7 @@ import {
type OptimizeGeometryPreflightResult,
} from './plan-geometry-preflight';
import {
canonicalizeConfigGeometry, canonicalizePosition, formatLatticeShiftCm,
canonicalizeConfigGeometry, canonicalizePosition,
} from './coordinate-canonicalization';
import { enqueueSerializedWrite, type OptimisticAttempt } from './serialized-write-queue';
import { applyCalibrationProposal, saveAutomaticCalibration, saveManualCalibration, saveVacuumMatrix,
@@ -440,7 +440,7 @@ export interface HouseplanEditorHostPort {
_adoptAuthoritative: (input: GatedAdoptionInput) => Promise<GatedAdoptionResult>; // #500: the one adoption entry
_rollbackOptimistic: (attempt: OptimisticAttempt<ServerConfig>) => boolean;
readonly _adoption: ConfigAdoption; // #500: identity owner; _serverCfg/_cfgRev/_layout/_layoutRev delegate to it
_alignDialog: { report: OptimizeReport; config: any; layout: Record<string, any>; preflight: OptimizeGeometryPreflightResult | null; cm: number; where: string; changed: boolean; busy: boolean; removeLiveMissingPositions: boolean; } | null;
_alignDialog: OptimizePlansDialogState | null;
_alignPoint: number[] | null;
_allRoomsFlat: () => { value: string; label: string; }[];
_angleField: (value: unknown) => string;
@@ -668,7 +668,6 @@ export interface HouseplanEditorHostPort {
_pointerModality: PointerModalityController;
_pointers: Map<number, { x: number; y: number; }>;
_pos: (d: DevItem) => { x: number; y: number; };
_preflightClipboardFallback: string | null;
_prepareModeTransition: (request: number, from: ModeVisualState, targetMode: HouseplanMode, targetZoom: number, targetCenterX?: number, targetCenterY?: number) => void;
_reducedMotion: boolean;
_rawPhysicalBodiesR: () => number[][][];
@@ -678,7 +677,6 @@ export interface HouseplanEditorHostPort {
_renderCardPreview: (spaceScale: number, nameScale: number, labelScale: number) => TemplateResult;
_renderCompass: () => TemplateResult;
_renderPlanHass: any;
_reportedPreflightFingerprint: string | null;
_resign: () => void;
_resize: ResizeController<
ResizePreview, ResizeLiveLabel[], SpaceGeometryState, ResizeWallUnion, ResizeWallArtifact
@@ -810,6 +808,8 @@ export class HouseplanEditorRuntime {
// к контроллеру оттуда. Остальной класс использует `public _x` — поле
// следует той же договорённости, а не заводит обёртку ради одного слова.
public readonly _radarSetup: RadarSetupController;
/** #642: «Оптимизировать планы» — свой модуль с узким портом, не делегаты карточки. */
public readonly optimizePlans: OptimizePlansDialog;
public constructor(public readonly host: HouseplanEditorHostPort) {
this._decorImages = new DecorImageEditor(host, {
decorSnap: (raw, pointerType) => this._decorSnap(raw, pointerType),
@@ -836,6 +836,26 @@ export class HouseplanEditorRuntime {
},
confirmDiscard: () => host._confirmDanger(radarDiscardRequest((key) => host._t(key))),
});
this.optimizePlans = new OptimizePlansDialog({
dialog: () => host._alignDialog,
setDialog: (next) => { host._alignDialog = next; },
t: (key, vars) => host._t(key, vars),
hass: () => host.hass,
config: () => host._serverCfg,
planReady: () => !!host._norm,
layout: () => host._layout,
integrationVersion: () => host._haIntegrationVersion,
cardVersion: () => displayVersion(CARD_VERSION),
requestUpdate: () => host.requestUpdate(),
showToast: (message) => host._showToast(message),
errorText: (error) => host._errText(error),
checkGeometry: (config) => host._checkOptimizeGeometry(config),
referenceContext: (removeLive) => this._optimizeReferenceContext(removeLive),
showMigrationBlocked: (error) => this._showWallModelMigrationBlocked(error),
clearGeometryGesture: () => this._clearGeometryGesture(),
commit: (config, layout) => commitPlanOptimization(host, config, layout),
reloadAfterConflict: () => Promise.all([host._reloadConfigOnly(true), host._reloadLayoutOnly()]),
});
host._editorSecondary = new EditorSecondaryController({
root: () => host.renderRoot as ShadowRoot,
requestUpdate: () => host.requestUpdate(),
@@ -8334,7 +8354,7 @@ public _saveSpaceCopy(): Promise<void> {
return saveSpaceCopy(this.host, {
clearGeometryGesture: () => this._clearGeometryGesture(),
optimizeReferenceContext: () => this._optimizeReferenceContext(false),
reportPreflightFailure: (result, config) => this._reportPreflightFailure(result, config),
reportPreflightFailure: (result, config) => this.optimizePlans.reportPreflightFailure(result, config),
saveConfigNow: (attempt) => this._saveConfigNow(attempt),
setMode: () => this._setMode('plan'),
showWallModelMigrationBlocked: (error) => this._showWallModelMigrationBlocked(error),
@@ -8931,67 +8951,6 @@ public _renderSupportDialog(): TemplateResult {
</hp-dialog>`;
}
public _preflightDiagnostics(
preflight: OptimizeGeometryPreflightResult,
candidate: ServerConfig | null,
): object {
// CODE-REVIEW-295-r1 M1: hash the CANDIDATE spaces the preflight judged,
// not the already-saved config — a saved-config hash is exactly what a
// space export would reproduce, and the block promises what the export
// does not carry.
const spacesById = new Map(((candidate as any)?.spaces || [])
.map((space) => [String(space?.id || ''), space]));
return {
kind: 'houseplan-optimize-preflight',
origin: 'runtime',
cardVersion: displayVersion(CARD_VERSION),
checkedAt: new Date().toISOString(),
preflightFingerprint: preflight.fingerprint,
failures: preflight.failures.map((failure) => ({
spaceId: failure.spaceId,
displayName: failure.displayName,
reason: failure.reason,
detail: failure.detail ?? null,
spaceGeometryFingerprint: spacesById.has(failure.spaceId)
? spacePhysicalGeometryFingerprint(spacesById.get(failure.spaceId))
: null,
})),
};
}
public _reportPreflightFailure(
preflight: OptimizeGeometryPreflightResult,
candidate: ServerConfig | null,
): void {
if (preflight.ok || preflight.fingerprint === this.host._reportedPreflightFingerprint) return;
this.host._reportedPreflightFingerprint = preflight.fingerprint;
// eslint-disable-next-line no-console
console.warn('[houseplan] optimize preflight failed', this._preflightDiagnostics(preflight, candidate));
}
public _preflightVersionsDiffer(): boolean {
const integration = this.host._haIntegrationVersion;
return typeof integration === 'string' && integration.length > 0
&& integration !== displayVersion(CARD_VERSION);
}
public async _copyPreflightDiagnostics(): Promise<void> {
const preflight = this.host._alignDialog?.preflight;
if (!preflight || preflight.ok) return;
const text = JSON.stringify(
this._preflightDiagnostics(preflight, this.host._alignDialog?.config ?? null), null, 2,
);
try {
await navigator.clipboard.writeText(text);
this.host._preflightClipboardFallback = null;
this.host._showToast(this.host._t('gs.preflight_copied'));
} catch {
// Insecure context / embedded webview: surface the block inline so the
// owner can select and copy it by hand.
this.host._preflightClipboardFallback = text;
}
}
public _checkOptimizeGeometryImpl(config: ServerConfig): OptimizeGeometryPreflightResult {
return checkOptimizeGeometry(config, {
fallbackSpaceName: (index) => this.host._t('gs.align_preflight_space', {
@@ -9075,90 +9034,6 @@ public _optimizeReferenceContext(
};
}
public _previewAlignDialog(removeLiveMissingPositions: boolean): void {
if (!this.host._norm || !this.host._serverCfg) return;
const spaces = this.host._serverCfg.spaces || [];
let r;
try {
r = optimizePlans(
this.host._serverCfg,
this.host._layout || {},
this._optimizeReferenceContext(removeLiveMissingPositions),
);
} catch (error) {
this._showWallModelMigrationBlocked(error);
return;
}
const preflight = r.changed ? this.host._checkOptimizeGeometry(r.config) : null;
if (preflight) this._reportPreflightFailure(preflight, r.config);
// The maximum geometry shift is an UPPER BOUND, not a sample. The run
// measured every element in the centimetres of ITS OWN space — converting
// one normalised maximum through the first space's `cell_cm` understated
// a two-scale plan twentyfold (AUD-158B1-01) — and the last tenth is
// rounded UP, so the promise can never be smaller than the deed.
const cm = Math.ceil(r.report.maxShiftCm * 10) / 10;
const sp = spaces.find((x: any) => x?.id != null && String(x.id) === r.report.maxSpace);
const where = spaces.length > 1 && sp ? String(sp.title || sp.id) : '';
// CODE-REVIEW-295-r1 M2: the inline clipboard fallback belongs to one
// dialog showing — a reopened dialog must not display the previous
// refusal's JSON while the visible reasons already describe a new one.
this.host._preflightClipboardFallback = null;
this.host._alignDialog = {
report: r.report, config: r.config, layout: r.layout, cm, where,
preflight, changed: r.changed, busy: false, removeLiveMissingPositions,
};
}
public _openAlignDialog = (): void => this._previewAlignDialog(false);
public _toggleOptimizeLivePositions = (): void => {
const dialog = this.host._alignDialog;
if (!dialog || dialog.busy || !dialog.report.liveMissingPositions.length) return;
this._previewAlignDialog(!dialog.removeLiveMissingPositions);
};
public async _runAlignToGrid(): Promise<void> {
let d = this.host._alignDialog;
if (!d || d.busy || !this.host._serverCfg || !d.changed || !d.preflight?.ok) return;
const fingerprint = contentFingerprint(d.config);
if (d.preflight.fingerprint !== fingerprint) {
const preflight = this.host._checkOptimizeGeometry(d.config);
this._reportPreflightFailure(preflight, d.config);
d = { ...d, preflight };
this.host._alignDialog = d;
if (!preflight.ok) return;
}
this._clearGeometryGesture();
this.host._alignDialog = { ...d, busy: true };
try {
await commitPlanOptimization(this.host, d.config, d.layout);
this.host._alignDialog = null;
this.host._preflightClipboardFallback = null;
this.host._showToast(this.host._t('gs.align_done', {
n: String(d.report.moved),
m: String(d.report.migrated + d.report.canonicalized
+ d.report.coordsCanonicalized + d.report.latticeCoordinatesCanonicalized
+ d.report.wallsMerged + d.report.spansMerged
+ d.report.partitionsMerged + d.report.partitionsReconciled
+ d.report.openingsRehosted + d.report.wallsStraightened),
r: String(d.report.spaceRefsRemapped + d.report.roomRefsRemapped
+ d.report.positionsRemapped + d.report.markersDetached
+ d.report.orphanRoomLabelsRemoved + d.report.orphanDevicePositionsRemoved
+ d.report.orphanGroupPositionsRemoved),
}));
} catch (e: any) {
if (this.host._alignDialog) this.host._alignDialog = { ...this.host._alignDialog, busy: false };
if (e?.code === 'wall_model_client_outdated') {
this.host._showToast(this.host._t('toast.wall_model_client_outdated'));
return;
}
if (e?.code === 'conflict') {
await Promise.all([this.host._reloadConfigOnly(true), this.host._reloadLayoutOnly()]);
}
this.host._showToast(this.host._t('toast.error', { err: this.host._errText(e) }));
}
}
public async _undoPlanOptimization(): Promise<void> {
if (!this.host._canOptimizeUndo || this.host._optimizeUndoBusy) return;
const undoKind = this.host._undoKind;
@@ -9689,243 +9564,6 @@ public _rangeInput(
?disabled=${disabled} aria-label=${ariaLabel || nothing} @input=${h} />`;
}
public _renderAlignDialog(): TemplateResult {
const d = this.host._alignDialog!;
const r = d.report;
const failed = d.changed && !d.preflight?.ok;
const failures = d.preflight?.failures || [];
const visibleNames = failures.slice(0, 3).map((failure) => failure.displayName);
const spaces = visibleNames.length
? visibleNames.join(', ')
: this.host._t('gs.align_preflight_space', { n: '1' });
const remaining = Math.max(0, failures.length - visibleNames.length);
const more = remaining
? this.host._t('gs.align_preflight_more', { n: String(remaining) })
: '';
const repaired = r.spaceRefsRemapped + r.roomRefsRemapped
+ r.positionsRemapped + r.markersDetached;
const modelMaintenance = r.migrated + r.canonicalized + r.coordsCanonicalized
+ r.wallSegmentsMigrated
+ r.roomDraftsMigrated + r.roomDraftSegmentsMigrated
+ r.wallsMerged + r.spansMerged + r.partitionsMerged
+ r.partitionsReconciled + r.openingsRehosted;
const gridWarning = r.moved + r.rotated + r.coordsCanonicalized + r.wallsStraightened;
const straightenCm = Math.ceil(r.maxStraightenShiftCm * 10) / 10;
const straightenSpace = (this.host._serverCfg?.spaces || []).find(
(space) => String(space?.id || '') === r.maxStraightenSpace,
);
const straightenWhere = (this.host._serverCfg?.spaces || []).length > 1 && straightenSpace
? String(straightenSpace.title || straightenSpace.id) : '';
const removed = r.orphanRoomLabelsRemoved + r.orphanDevicePositionsRemoved
+ r.orphanGroupPositionsRemoved;
const liveNames = r.liveMissingPositions.map((item) => item.name).filter(Boolean);
const visibleLiveNames = liveNames.slice(0, 3).join(', ');
const remainingLiveNames = Math.max(0, liveNames.length - 3);
const liveNamesText = visibleLiveNames
? this.host._t('gs.optimize_live_names', {
names: visibleLiveNames,
more: remainingLiveNames
? this.host._t('gs.optimize_reference_more', { n: String(remainingLiveNames) }) : '',
})
: '';
const registryLimited = r.unverifiedPositions.some(
(item) => item.reason === 'registry_unavailable',
);
const detailStatus = (item: typeof r.removedPositions[number]): string => {
if (r.removedPositions.some((removedItem) => removedItem.id === item.id)) {
return this.host._t('gs.optimize_detail_removed');
}
if (r.liveMissingPositions.some((liveItem) => liveItem.id === item.id)) {
return this.host._t('gs.optimize_detail_live');
}
return this.host._t('gs.optimize_detail_unverified');
};
const detailKind = (kind: typeof r.removedPositions[number]['kind']): string => this.host._t(
kind === 'room_label' ? 'gs.optimize_detail_room_label'
: kind === 'group' ? 'gs.optimize_detail_group'
: kind === 'device' ? 'gs.optimize_detail_device'
: 'gs.optimize_detail_unknown',
);
const referenceDetails = [
...r.removedPositions,
...r.liveMissingPositions.filter((item) => (
!r.removedPositions.some((removedItem) => removedItem.id === item.id)
)),
...r.unverifiedPositions,
];
const visibleDetails = referenceDetails.slice(0, 10);
const remainingDetails = Math.max(0, referenceDetails.length - visibleDetails.length);
return html`<hp-dialog .hass=${this.host.hass} data-kind="settings" .title=${this.host._t('gs.align_title')} icon="mdi:broom"
dismiss-on-scrim @hp-close=${() => { this.host._alignDialog = null; this.host._preflightClipboardFallback = null; }}>
<div class="body">
${failed
? html`
<p class="alignmsg">${this.host._t('gs.align_preflight_failed', { spaces, more })}</p>
${failures.slice(0, 10).map((failure) => html`<p class="alignmsg">
${failure.displayName}: ${this.host._t(`gs.preflight_reason_${failure.reason}` as I18nKey)}
</p>`)}
${failures.length > 10 ? html`<p class="alignmsg">
${this.host._t('gs.align_preflight_more', { n: String(failures.length - 10) })}
</p>` : nothing}
<div class="rhint">${this.host._t('gs.align_preflight_hint')}</div>
${this._preflightVersionsDiffer() ? html`
<div class="rhint">${this.host._t('gs.preflight_update_hint')}</div>` : nothing}
<div class="row">
<button class="btn ghost" @click=${() => this._copyPreflightDiagnostics()}>
<ha-icon icon="mdi:content-copy"></ha-icon>
${this.host._t('gs.preflight_copy')}
</button>
</div>
${this.host._preflightClipboardFallback ? html`<details open>
<summary>${this.host._t('gs.preflight_copy')}</summary>
<pre style="user-select:text;white-space:pre-wrap">${this.host._preflightClipboardFallback}</pre>
</details>` : nothing}`
: !d.changed
? html`<p class="alignmsg">${this.host._t(
r.liveMissingPositions.length || r.unverifiedPositions.length
|| r.nestedRefsUnresolved
? 'gs.optimize_no_automatic_changes' : 'gs.align_none',
)}</p>`
: html`
${r.moved ? html`<p class="alignmsg">${this.host._t('gs.align_count', {
n: String(r.moved), total: String(r.total), cm: String(d.cm),
})}</p>` : nothing}
${r.latticeCoordinatesCanonicalized ? html`
<p class="alignmsg">${this.host._t('gs.optimize_lattice_summary', {
n: String(r.latticeCoordinatesCanonicalized),
cm: formatLatticeShiftCm(r.latticeMaxShiftCm),
})}</p>
${r.latticeSpaces.map((space) => html`<p class="alignmsg">${this.host._t(
'gs.optimize_lattice_space', {
space: space.space,
n: String(space.canonicalized),
far: String(space.far),
},
)}</p>`)}
` : nothing}
${d.where
? html`<p class="alignmsg">${this.host._t('gs.align_where', { s: d.where })}</p>`
: nothing}
${r.rotated
? html`<p class="alignmsg">${this.host._t('gs.align_turned', { n: String(r.rotated) })}</p>`
: nothing}
${r.wallSegmentsMigrated ? html`<p class="alignmsg">${this.host._t(
'gs.wall_segments_migrated', { n: String(r.wallSegmentsMigrated) },
)}</p>` : nothing}
${r.roomDraftsMigrated || r.roomDraftSegmentsMigrated ? html`
<p class="alignmsg">${this.host._t('gs.room_drafts_migrated', {
drafts: String(r.roomDraftsMigrated),
segments: String(r.roomDraftSegmentsMigrated),
})}</p>` : nothing}
${r.legacyZeroWallsMigrated ? html`<p class="alignmsg">${this.host._t(
'gs.zero_walls_migrated', { n: String(r.legacyZeroWallsMigrated) },
)}</p>` : nothing}
${modelMaintenance ? html`<p class="alignmsg">${this.host._t('gs.optimize_changes', {
m: String(r.migrated), c: String(r.canonicalized),
p: String(r.coordsCanonicalized), w: String(r.wallsMerged),
s: String(r.spansMerged), i: String(r.partitionsMerged),
})}</p>` : nothing}
${r.partitionsReconciled ? html`<p class="alignmsg">${this.host._t(
'gs.optimize_coincident_partitions', { n: String(r.partitionsReconciled) },
)}</p>` : nothing}
${r.openingsRehosted ? html`<p class="alignmsg">${this.host._t(
'gs.optimize_openings_rehosted', { n: String(r.openingsRehosted) },
)}</p>` : nothing}
${r.wallsStraightened ? html`<p class="alignmsg">${this.host._t(
'gs.optimize_walls_straightened', {
n: String(r.wallsStraightened), cm: String(straightenCm),
},
)}</p>` : nothing}
${straightenWhere ? html`<p class="alignmsg">${this.host._t(
'gs.optimize_walls_straightened_where', { s: straightenWhere },
)}</p>` : nothing}
${r.glowSpacesMigrated || r.glowRoomsMigrated
? html`<p class="alignmsg">${this.host._t('gs.optimize_glow_migration', {
spaces: String(r.glowSpacesMigrated),
rooms: String(r.glowRoomsMigrated),
})}</p>`
: nothing}
${gridWarning ? html`<div class="rhint">${this.host._t('gs.align_warn')}</div>` : nothing}`}
${!failed && r.wallsStraightenSkipped ? html`<p class="rhint">${this.host._t(
'gs.optimize_walls_straighten_skipped', {
n: String(r.wallsStraightenSkipped),
},
)}</p>` : nothing}
${repaired
? html`<p class="alignmsg">${this.host._t('gs.optimize_references', {
spaces: String(r.spaceRefsRemapped), rooms: String(r.roomRefsRemapped),
positions: String(r.positionsRemapped), detached: String(r.markersDetached),
})}</p>`
: nothing}
${removed
? html`<p class="alignmsg">${this.host._t('gs.optimize_orphans_removed', {
total: String(removed),
rooms: String(r.orphanRoomLabelsRemoved),
devices: String(r.orphanDevicePositionsRemoved),
groups: String(r.orphanGroupPositionsRemoved),
})}</p>`
: nothing}
${r.liveMissingPositions.length
? html`<div class="optimize-live">
<p class="alignmsg">${this.host._t(d.removeLiveMissingPositions
? 'gs.optimize_live_positions_remove' : 'gs.optimize_live_positions', {
n: String(r.liveMissingPositions.length), names: liveNamesText,
})}</p>
<button class="btn ghost optimize-cleanup" type="button"
aria-pressed=${d.removeLiveMissingPositions ? 'true' : 'false'}
@click=${() => this._toggleOptimizeLivePositions()} ?disabled=${d.busy}>
<ha-icon icon=${d.removeLiveMissingPositions ? 'mdi:undo' : 'mdi:map-marker-remove-outline'}></ha-icon>
${this.host._t(d.removeLiveMissingPositions
? 'gs.optimize_live_keep' : 'gs.optimize_live_remove')}
</button>
${d.removeLiveMissingPositions
? html`<div class="rhint optimize-selected" role="status">
${this.host._t('gs.optimize_live_selected')}
</div>`
: nothing}
</div>`
: nothing}
${r.unverifiedPositions.length
? html`<div class="rhint" role="alert">
${this.host._t('gs.optimize_unverified', {
n: String(r.unverifiedPositions.length),
})}
${registryLimited ? ` ${this.host._t('gs.optimize_registry_limited')}` : ''}
</div>`
: nothing}
${r.nestedRefsUnresolved
? html`<div class="rhint" role="alert">${this.host._t('gs.optimize_vacuum_warning', {
n: String(r.nestedRefsUnresolved),
})}</div>`
: nothing}
${referenceDetails.length
? html`<details class="optimize-details">
<summary>${this.host._t('gs.optimize_details')}</summary>
<ul>
${visibleDetails.map((item) => html`<li>${this.host._t('gs.optimize_detail_item', {
status: detailStatus(item), kind: detailKind(item.kind),
id: item.id, space: item.spaceId,
})}</li>`)}
</ul>
${remainingDetails
? html`<div class="rhint">${this.host._t('gs.optimize_details_more', {
n: String(remainingDetails),
})}</div>`
: nothing}
</details>`
: nothing}
</div>
<div class="row" slot="footer">
<span class="spacer"></span>
<button class="btn ghost" data-hp="dialog-cancel" @click=${() => { this.host._alignDialog = null; this.host._preflightClipboardFallback = null; }}>${this.host._t('btn.cancel')}</button>
${!d.changed || !d.preflight?.ok ? nothing : html`
<button class="btn on" data-hp="dialog-confirm" @click=${() => this._runAlignToGrid()} ?disabled=${d.busy}>
<ha-icon icon="mdi:check"></ha-icon>${d.busy ? '…' : this.host._t('gs.align_run')}
</button>`}
</div>
</hp-dialog>`;
}
public _renderSettingsDialog(): TemplateResult { return renderGeneralSettingsDialog.call(this); }
public _openRulesDialog = (): void => {
+492
View File
@@ -0,0 +1,492 @@
/**
* Диалог «Оптимизировать планы» (Общие настройки → Сетка), #642.
*
* Первый вынос из монолита по образцу `live-*` и `RadarSetupController`: узкий
* порт `OptimizePlansDialogPort` из 18 членов вместо `HouseplanEditorHostPort`
* на 350, без делегатов-заглушек в карточке. Модуль импортирует только
* редакторский рантайм, поэтому остаётся в его ленивом чанке; карточка берёт
* отсюда только тип состояния.
*
* Сам диалог (`_alignDialog`) остаётся `@state` карточки: харнесс и golden
* пишут его напрямую. Сюда переехали два куска состояния, которые карточке
* не принадлежали: инлайн-фолбэк буфера обмена и дедуп dev-лога.
*/
import { html, nothing, type TemplateResult } from 'lit';
import { formatLatticeShiftCm } from './coordinate-canonicalization';
import type { DeviceLayout } from './device-position-history';
import type { I18nKey } from './i18n';
import {
spacePhysicalGeometryFingerprint, type OptimizeGeometryPreflightResult,
} from './plan-geometry-preflight';
import { optimizePlans, type OptimizeReport } from './plan-optimizer';
import type { SpaceReferenceRepairContext } from './space-reference-repair';
import type { ServerConfig } from './types';
import { contentFingerprint } from './visual-continuity';
/** Optimization preview plus the exact pair, so commit cannot differ from it. */
export type OptimizePlansDialogState = {
report: OptimizeReport; config: any; layout: Record<string, any>;
preflight: OptimizeGeometryPreflightResult | null;
/** the promised maximum, in centimetres, ALREADY rounded up (AUD-158B1-01) */
cm: number;
/** the space that maximum belongs to, named only when there are several */
where: string;
changed: boolean;
busy: boolean;
/** False by default; true only after the secondary preview action. */
removeLiveMissingPositions: boolean;
};
/**
* Всё, что диалогу нужно от редактора. Порт собирает рантайм в конструкторе;
* других зависимостей у модуля нет. Рост порта выше 20 членов — сигнал, что
* сюда протекает чужая подсистема (юнит `optimize-plans-dialog.test.mjs`).
*/
export interface OptimizePlansDialogPort {
dialog(): OptimizePlansDialogState | null;
setDialog(next: OptimizePlansDialogState | null): void;
t(key: I18nKey, vars?: Record<string, string | number>): string;
hass(): unknown;
config(): ServerConfig | null;
/** the normalised plan exists — the preview has something to optimise */
planReady(): boolean;
layout(): DeviceLayout | null | undefined;
integrationVersion(): string | null;
/** `displayVersion(CARD_VERSION)` of the runtime; no second copy here */
cardVersion(): string;
requestUpdate(): void;
showToast(message: string): void;
errorText(error: unknown): string;
checkGeometry(config: ServerConfig): OptimizeGeometryPreflightResult;
referenceContext(removeLiveMissingPositions: boolean): SpaceReferenceRepairContext;
showMigrationBlocked(error: unknown): void;
clearGeometryGesture(): void;
commit(config: ServerConfig, layout: DeviceLayout): Promise<void>;
/** a write conflict reloads both stores before the error toast */
reloadAfterConflict(): Promise<unknown>;
}
export function preflightDiagnostics(
preflight: OptimizeGeometryPreflightResult,
candidate: ServerConfig | null,
{ cardVersion, now }: { cardVersion: string; now: Date },
): object {
// CODE-REVIEW-295-r1 M1: hash the CANDIDATE spaces the preflight judged,
// not the already-saved config — a saved-config hash is exactly what a
// space export would reproduce, and the block promises what the export
// does not carry.
const spacesById = new Map(((candidate as any)?.spaces || []) // any-ok: кандидат приходит из untyped optimizePlans, форма spaces не гарантирована
.map((space) => [String(space?.id || ''), space]));
return {
kind: 'houseplan-optimize-preflight',
origin: 'runtime',
cardVersion,
checkedAt: now.toISOString(),
preflightFingerprint: preflight.fingerprint,
failures: preflight.failures.map((failure) => ({
spaceId: failure.spaceId,
displayName: failure.displayName,
reason: failure.reason,
detail: failure.detail ?? null,
spaceGeometryFingerprint: spacesById.has(failure.spaceId)
? spacePhysicalGeometryFingerprint(spacesById.get(failure.spaceId))
: null,
})),
};
}
export function preflightVersionsDiffer(integration: string | null, cardVersion: string): boolean {
return typeof integration === 'string' && integration.length > 0
&& integration !== cardVersion;
}
export class OptimizePlansDialog {
/**
* CODE-REVIEW-295-r1 M2: the inline clipboard fallback belongs to one
* dialog showing. Keyed by the dialog object, a reopened dialog — always a
* new object — can never display the previous refusal's JSON, and closing
* needs no manual reset.
*/
private readonly fallbacks = new WeakMap<object, string>();
/** #295: dev-log once per distinct failing preflight, not once per render. */
private reportedFingerprint: string | null = null;
public constructor(private readonly port: OptimizePlansDialogPort) {}
/** #295: diagnostics text shown inline when the clipboard is unavailable. */
public get clipboardFallback(): string | null {
const dialog = this.port.dialog();
return dialog ? this.fallbacks.get(dialog) ?? null : null;
}
private diagnostics(
preflight: OptimizeGeometryPreflightResult,
candidate: ServerConfig | null,
): object {
return preflightDiagnostics(preflight, candidate, {
cardVersion: this.port.cardVersion(), now: new Date(),
});
}
public reportPreflightFailure(
preflight: OptimizeGeometryPreflightResult,
candidate: ServerConfig | null,
): void {
if (preflight.ok || preflight.fingerprint === this.reportedFingerprint) return;
this.reportedFingerprint = preflight.fingerprint;
// eslint-disable-next-line no-console
console.warn('[houseplan] optimize preflight failed', this.diagnostics(preflight, candidate));
}
public async copyDiagnostics(): Promise<void> {
const dialog = this.port.dialog();
const preflight = dialog?.preflight;
if (!dialog || !preflight || preflight.ok) return;
const text = JSON.stringify(
this.diagnostics(preflight, dialog.config ?? null), null, 2,
);
try {
await navigator.clipboard.writeText(text);
if (this.fallbacks.delete(dialog)) this.port.requestUpdate();
this.port.showToast(this.port.t('gs.preflight_copied'));
} catch {
// Insecure context / embedded webview: surface the block inline so the
// owner can select and copy it by hand.
this.fallbacks.set(dialog, text);
this.port.requestUpdate();
}
}
/**
* Preview whole-plan maintenance. Nothing is written here: the pure run
* produces both the report and the exact config/layout pair to commit.
*/
public preview(removeLiveMissingPositions: boolean): void {
const config = this.port.config();
if (!this.port.planReady() || !config) return;
const spaces = config.spaces || [];
let r;
try {
r = optimizePlans(
config,
this.port.layout() || {},
this.port.referenceContext(removeLiveMissingPositions),
);
} catch (error) {
this.port.showMigrationBlocked(error);
return;
}
const preflight = r.changed ? this.port.checkGeometry(r.config) : null;
if (preflight) this.reportPreflightFailure(preflight, r.config);
// The maximum geometry shift is an UPPER BOUND, not a sample. The run
// measured every element in the centimetres of ITS OWN space — converting
// one normalised maximum through the first space's `cell_cm` understated
// a two-scale plan twentyfold (AUD-158B1-01) — and the last tenth is
// rounded UP, so the promise can never be smaller than the deed.
const cm = Math.ceil(r.report.maxShiftCm * 10) / 10;
const sp = spaces.find((x) => x?.id != null && String(x.id) === r.report.maxSpace);
const where = spaces.length > 1 && sp ? String(sp.title || sp.id) : '';
// A new dialog object also starts without an inline clipboard fallback.
this.port.setDialog({
report: r.report, config: r.config, layout: r.layout, cm, where,
preflight, changed: r.changed, busy: false, removeLiveMissingPositions,
});
}
public open(): void { this.preview(false); }
public toggleLivePositions(): void {
const dialog = this.port.dialog();
if (!dialog || dialog.busy || !dialog.report.liveMissingPositions.length) return;
this.preview(!dialog.removeLiveMissingPositions);
}
/**
* The backend persists an intent before either store changes, then keeps a
* one-deep snapshot that remains undoable until the next plan edit.
*/
public async run(): Promise<void> {
let d = this.port.dialog();
if (!d || d.busy || !this.port.config() || !d.changed || !d.preflight?.ok) return;
const fingerprint = contentFingerprint(d.config);
if (d.preflight.fingerprint !== fingerprint) {
const preflight = this.port.checkGeometry(d.config);
this.reportPreflightFailure(preflight, d.config);
d = { ...d, preflight };
this.port.setDialog(d);
if (!preflight.ok) return;
}
this.port.clearGeometryGesture();
this.port.setDialog({ ...d, busy: true });
try {
await this.port.commit(d.config, d.layout);
this.port.setDialog(null);
this.port.showToast(this.port.t('gs.align_done', {
n: String(d.report.moved),
m: String(d.report.migrated + d.report.canonicalized
+ d.report.coordsCanonicalized + d.report.latticeCoordinatesCanonicalized
+ d.report.wallsMerged + d.report.spansMerged
+ d.report.partitionsMerged + d.report.partitionsReconciled
+ d.report.openingsRehosted + d.report.wallsStraightened),
r: String(d.report.spaceRefsRemapped + d.report.roomRefsRemapped
+ d.report.positionsRemapped + d.report.markersDetached
+ d.report.orphanRoomLabelsRemoved + d.report.orphanDevicePositionsRemoved
+ d.report.orphanGroupPositionsRemoved),
}));
} catch (e: any) { // any-ok: ответ WS HA не типизирован, code читается как у всех писателей конфига
const current = this.port.dialog();
if (current) this.port.setDialog({ ...current, busy: false });
if (e?.code === 'wall_model_client_outdated') {
this.port.showToast(this.port.t('toast.wall_model_client_outdated'));
return;
}
if (e?.code === 'conflict') {
await this.port.reloadAfterConflict();
}
this.port.showToast(this.port.t('toast.error', { err: this.port.errorText(e) }));
}
}
/**
* The confirmation separates geometry movement from lossless maintenance,
* and promises the one-deep undo before either store is changed.
*/
render(): TemplateResult {
const d = this.port.dialog()!;
const r = d.report;
const failed = d.changed && !d.preflight?.ok;
const failures = d.preflight?.failures || [];
const visibleNames = failures.slice(0, 3).map((failure) => failure.displayName);
const spaces = visibleNames.length
? visibleNames.join(', ')
: this.port.t('gs.align_preflight_space', { n: '1' });
const remaining = Math.max(0, failures.length - visibleNames.length);
const more = remaining
? this.port.t('gs.align_preflight_more', { n: String(remaining) })
: '';
const repaired = r.spaceRefsRemapped + r.roomRefsRemapped
+ r.positionsRemapped + r.markersDetached;
const modelMaintenance = r.migrated + r.canonicalized + r.coordsCanonicalized
+ r.wallSegmentsMigrated
+ r.roomDraftsMigrated + r.roomDraftSegmentsMigrated
+ r.wallsMerged + r.spansMerged + r.partitionsMerged
+ r.partitionsReconciled + r.openingsRehosted;
const gridWarning = r.moved + r.rotated + r.coordsCanonicalized + r.wallsStraightened;
const straightenCm = Math.ceil(r.maxStraightenShiftCm * 10) / 10;
const straightenSpace = (this.port.config()?.spaces || []).find(
(space) => String(space?.id || '') === r.maxStraightenSpace,
);
const straightenWhere = (this.port.config()?.spaces || []).length > 1 && straightenSpace
? String(straightenSpace.title || straightenSpace.id) : '';
const removed = r.orphanRoomLabelsRemoved + r.orphanDevicePositionsRemoved
+ r.orphanGroupPositionsRemoved;
const liveNames = r.liveMissingPositions.map((item) => item.name).filter(Boolean);
const visibleLiveNames = liveNames.slice(0, 3).join(', ');
const remainingLiveNames = Math.max(0, liveNames.length - 3);
const liveNamesText = visibleLiveNames
? this.port.t('gs.optimize_live_names', {
names: visibleLiveNames,
more: remainingLiveNames
? this.port.t('gs.optimize_reference_more', { n: String(remainingLiveNames) }) : '',
})
: '';
const registryLimited = r.unverifiedPositions.some(
(item) => item.reason === 'registry_unavailable',
);
const detailStatus = (item: typeof r.removedPositions[number]): string => {
if (r.removedPositions.some((removedItem) => removedItem.id === item.id)) {
return this.port.t('gs.optimize_detail_removed');
}
if (r.liveMissingPositions.some((liveItem) => liveItem.id === item.id)) {
return this.port.t('gs.optimize_detail_live');
}
return this.port.t('gs.optimize_detail_unverified');
};
const detailKind = (kind: typeof r.removedPositions[number]['kind']): string => this.port.t(
kind === 'room_label' ? 'gs.optimize_detail_room_label'
: kind === 'group' ? 'gs.optimize_detail_group'
: kind === 'device' ? 'gs.optimize_detail_device'
: 'gs.optimize_detail_unknown',
);
const referenceDetails = [
...r.removedPositions,
...r.liveMissingPositions.filter((item) => (
!r.removedPositions.some((removedItem) => removedItem.id === item.id)
)),
...r.unverifiedPositions,
];
const visibleDetails = referenceDetails.slice(0, 10);
const remainingDetails = Math.max(0, referenceDetails.length - visibleDetails.length);
const fallback = this.clipboardFallback;
return html`<hp-dialog .hass=${this.port.hass()} data-kind="settings" .title=${this.port.t('gs.align_title')} icon="mdi:broom"
dismiss-on-scrim @hp-close=${() => this.port.setDialog(null)}>
<div class="body">
${failed
? html`
<p class="alignmsg">${this.port.t('gs.align_preflight_failed', { spaces, more })}</p>
${failures.slice(0, 10).map((failure) => html`<p class="alignmsg">
${failure.displayName}: ${this.port.t(`gs.preflight_reason_${failure.reason}` as I18nKey)}
</p>`)}
${failures.length > 10 ? html`<p class="alignmsg">
${this.port.t('gs.align_preflight_more', { n: String(failures.length - 10) })}
</p>` : nothing}
<div class="rhint">${this.port.t('gs.align_preflight_hint')}</div>
${preflightVersionsDiffer(this.port.integrationVersion(), this.port.cardVersion()) ? html`
<div class="rhint">${this.port.t('gs.preflight_update_hint')}</div>` : nothing}
<div class="row">
<button class="btn ghost" @click=${() => this.copyDiagnostics()}>
<ha-icon icon="mdi:content-copy"></ha-icon>
${this.port.t('gs.preflight_copy')}
</button>
</div>
${fallback ? html`<details open>
<summary>${this.port.t('gs.preflight_copy')}</summary>
<pre style="user-select:text;white-space:pre-wrap">${fallback}</pre>
</details>` : nothing}`
: !d.changed
? html`<p class="alignmsg">${this.port.t(
r.liveMissingPositions.length || r.unverifiedPositions.length
|| r.nestedRefsUnresolved
? 'gs.optimize_no_automatic_changes' : 'gs.align_none',
)}</p>`
: html`
${r.moved ? html`<p class="alignmsg">${this.port.t('gs.align_count', {
n: String(r.moved), total: String(r.total), cm: String(d.cm),
})}</p>` : nothing}
${r.latticeCoordinatesCanonicalized ? html`
<p class="alignmsg">${this.port.t('gs.optimize_lattice_summary', {
n: String(r.latticeCoordinatesCanonicalized),
cm: formatLatticeShiftCm(r.latticeMaxShiftCm),
})}</p>
${r.latticeSpaces.map((space) => html`<p class="alignmsg">${this.port.t(
'gs.optimize_lattice_space', {
space: space.space,
n: String(space.canonicalized),
far: String(space.far),
},
)}</p>`)}
` : nothing}
${d.where
? html`<p class="alignmsg">${this.port.t('gs.align_where', { s: d.where })}</p>`
: nothing}
${r.rotated
? html`<p class="alignmsg">${this.port.t('gs.align_turned', { n: String(r.rotated) })}</p>`
: nothing}
${r.wallSegmentsMigrated ? html`<p class="alignmsg">${this.port.t(
'gs.wall_segments_migrated', { n: String(r.wallSegmentsMigrated) },
)}</p>` : nothing}
${r.roomDraftsMigrated || r.roomDraftSegmentsMigrated ? html`
<p class="alignmsg">${this.port.t('gs.room_drafts_migrated', {
drafts: String(r.roomDraftsMigrated),
segments: String(r.roomDraftSegmentsMigrated),
})}</p>` : nothing}
${r.legacyZeroWallsMigrated ? html`<p class="alignmsg">${this.port.t(
'gs.zero_walls_migrated', { n: String(r.legacyZeroWallsMigrated) },
)}</p>` : nothing}
${modelMaintenance ? html`<p class="alignmsg">${this.port.t('gs.optimize_changes', {
m: String(r.migrated), c: String(r.canonicalized),
p: String(r.coordsCanonicalized), w: String(r.wallsMerged),
s: String(r.spansMerged), i: String(r.partitionsMerged),
})}</p>` : nothing}
${r.partitionsReconciled ? html`<p class="alignmsg">${this.port.t(
'gs.optimize_coincident_partitions', { n: String(r.partitionsReconciled) },
)}</p>` : nothing}
${r.openingsRehosted ? html`<p class="alignmsg">${this.port.t(
'gs.optimize_openings_rehosted', { n: String(r.openingsRehosted) },
)}</p>` : nothing}
${r.wallsStraightened ? html`<p class="alignmsg">${this.port.t(
'gs.optimize_walls_straightened', {
n: String(r.wallsStraightened), cm: String(straightenCm),
},
)}</p>` : nothing}
${straightenWhere ? html`<p class="alignmsg">${this.port.t(
'gs.optimize_walls_straightened_where', { s: straightenWhere },
)}</p>` : nothing}
${r.glowSpacesMigrated || r.glowRoomsMigrated
? html`<p class="alignmsg">${this.port.t('gs.optimize_glow_migration', {
spaces: String(r.glowSpacesMigrated),
rooms: String(r.glowRoomsMigrated),
})}</p>`
: nothing}
${gridWarning ? html`<div class="rhint">${this.port.t('gs.align_warn')}</div>` : nothing}`}
${!failed && r.wallsStraightenSkipped ? html`<p class="rhint">${this.port.t(
'gs.optimize_walls_straighten_skipped', {
n: String(r.wallsStraightenSkipped),
},
)}</p>` : nothing}
${repaired
? html`<p class="alignmsg">${this.port.t('gs.optimize_references', {
spaces: String(r.spaceRefsRemapped), rooms: String(r.roomRefsRemapped),
positions: String(r.positionsRemapped), detached: String(r.markersDetached),
})}</p>`
: nothing}
${removed
? html`<p class="alignmsg">${this.port.t('gs.optimize_orphans_removed', {
total: String(removed),
rooms: String(r.orphanRoomLabelsRemoved),
devices: String(r.orphanDevicePositionsRemoved),
groups: String(r.orphanGroupPositionsRemoved),
})}</p>`
: nothing}
${r.liveMissingPositions.length
? html`<div class="optimize-live">
<p class="alignmsg">${this.port.t(d.removeLiveMissingPositions
? 'gs.optimize_live_positions_remove' : 'gs.optimize_live_positions', {
n: String(r.liveMissingPositions.length), names: liveNamesText,
})}</p>
<button class="btn ghost optimize-cleanup" type="button"
aria-pressed=${d.removeLiveMissingPositions ? 'true' : 'false'}
@click=${() => this.toggleLivePositions()} ?disabled=${d.busy}>
<ha-icon icon=${d.removeLiveMissingPositions ? 'mdi:undo' : 'mdi:map-marker-remove-outline'}></ha-icon>
${this.port.t(d.removeLiveMissingPositions
? 'gs.optimize_live_keep' : 'gs.optimize_live_remove')}
</button>
${d.removeLiveMissingPositions
? html`<div class="rhint optimize-selected" role="status">
${this.port.t('gs.optimize_live_selected')}
</div>`
: nothing}
</div>`
: nothing}
${r.unverifiedPositions.length
? html`<div class="rhint" role="alert">
${this.port.t('gs.optimize_unverified', {
n: String(r.unverifiedPositions.length),
})}
${registryLimited ? ` ${this.port.t('gs.optimize_registry_limited')}` : ''}
</div>`
: nothing}
${r.nestedRefsUnresolved
? html`<div class="rhint" role="alert">${this.port.t('gs.optimize_vacuum_warning', {
n: String(r.nestedRefsUnresolved),
})}</div>`
: nothing}
${referenceDetails.length
? html`<details class="optimize-details">
<summary>${this.port.t('gs.optimize_details')}</summary>
<ul>
${visibleDetails.map((item) => html`<li>${this.port.t('gs.optimize_detail_item', {
status: detailStatus(item), kind: detailKind(item.kind),
id: item.id, space: item.spaceId,
})}</li>`)}
</ul>
${remainingDetails
? html`<div class="rhint">${this.port.t('gs.optimize_details_more', {
n: String(remainingDetails),
})}</div>`
: nothing}
</details>`
: nothing}
</div>
<div class="row" slot="footer">
<span class="spacer"></span>
<button class="btn ghost" data-hp="dialog-cancel" @click=${() => this.port.setDialog(null)}>${this.port.t('btn.cancel')}</button>
${!d.changed || !d.preflight?.ok ? nothing : html`
<button class="btn on" data-hp="dialog-confirm" @click=${() => this.run()} ?disabled=${d.busy}>
<ha-icon icon="mdi:check"></ha-icon>${d.busy ? '…' : this.port.t('gs.align_run')}
</button>`}
</div>
</hp-dialog>`;
}
}
+9 -2
View File
@@ -33,7 +33,11 @@ const CAPS = {
// ни карточка, ни харнесс (реализация живёт в рантайме и зовётся оттуда),
// 190 неиспользуемых импортов и 9 мёртвых констант — 784 строки. Потолок
// опущен на выигрыш, запас над фактом три строки.
'src/houseplan-card.ts': 12951,
// 2026-09-24, #642: диалог «Оптимизировать планы» уехал в
// src/optimize-plans-dialog.ts — ушли пять делегатов, две стрелки-заглушки,
// поле фолбэка буфера обмена, дедуп dev-лога и литерал типа состояния,
// 62 строки. Потолок опущен на выигрыш, запас прежний.
'src/houseplan-card.ts': 12889,
// #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.
@@ -45,7 +49,10 @@ const CAPS = {
// 2026-09-23, #624: снесены мёртвые копии объявлений карточки (warm-boot,
// LS_*, GLOW_*, debounce, navigate, lruRead — 31 объявление и 6 типов) и
// 196 неиспользуемых импортов — 400 строк. Потолок опущен на выигрыш.
'src/houseplan-editor-runtime.ts': 12359,
// 2026-09-24, #642: диалог «Оптимизировать планы» (превью, запуск,
// диагностика preflight, разметка) уехал в src/optimize-plans-dialog.ts за
// узкий порт из 18 членов — 362 строки. Потолок опущен на выигрыш.
'src/houseplan-editor-runtime.ts': 11997,
};
/**
+5 -13
View File
@@ -244,14 +244,8 @@ test('Optimize distinguishes updated spaces from cleaned coordinate noise', () =
);
assert.equal(en['gs.zero_walls_migrated'], 'Virtual wall spans converted: {n}.');
assert.equal(ru['gs.zero_walls_migrated'], 'Преобразовано виртуальных участков: {n}.');
assert.match(cardSource, /gs\.zero_walls_migrated/);
assert.match(cardSource, /p: String\(r\.coordsCanonicalized\)/);
// #229: the independent-wall counter is reported, not silently accumulated
assert.match(cardSource, /i: String\(r\.partitionsMerged\)/);
assert.match(cardSource, /gs\.optimize_coincident_partitions/);
assert.match(cardSource, /gs\.optimize_openings_rehosted/);
assert.match(cardSource,
/d\.report\.coordsCanonicalized \+ d\.report\.latticeCoordinatesCanonicalized/);
// #642: разметка и тост диалога проверяются исполнением —
// test/optimize-plans-dialog.test.mjs.
});
test('issue 306 zero-wall failures have dedicated symmetric copy', () => {
@@ -293,8 +287,8 @@ test('issue 291 Optimize reports lattice cleanup separately in both languages',
ru['gs.optimize_lattice_space'],
'{space}: канонизировано значений координат: {n}; оставлено значений вне сетки: {far}.',
);
assert.match(cardSource, /formatLatticeShiftCm\(r\.latticeMaxShiftCm\)/);
assert.match(cardSource, /r\.latticeSpaces\.map/);
// #642: вывод сдвига и строк по пространствам — исполнением в
// test/optimize-plans-dialog.test.mjs.
});
test('issue 252 Optimize keeps internal ids out of the main orphan report', () => {
@@ -306,9 +300,7 @@ test('issue 252 Optimize keeps internal ids out of the main orphan report', () =
ru['gs.optimize_orphans_removed'],
'Убрано забытых записей: {total} — подписи комнат: {rooms}; устройства: {devices}; групповые метки: {groups}. Все они принадлежали пространствам, удалённым ранее.',
);
assert.match(cardSource, /gs\.optimize_orphans_removed/);
assert.match(cardSource, /<details class="optimize-details">/);
assert.doesNotMatch(cardSource, /this\._t\('gs\.optimize_reference_warning'/);
// #642: сам отчёт диалога — исполнением в test/optimize-plans-dialog.test.mjs.
for (const key of [
'gs.optimize_orphans_removed', 'gs.optimize_live_positions',
'gs.optimize_unverified', 'gs.optimize_vacuum_warning',
+4 -2
View File
@@ -79,8 +79,10 @@ test('#550: assertion witnesses have no behavioural oracle hidden in setup', ()
test('#550: preflight saved-config mutant targets the editor host and still compiles', () => {
const mutant = MUTANTS.find(({ id }) => id === 'preflight-fingerprint-from-saved-config');
assert.ok(mutant);
assert.equal(mutant.patches[0].file, 'src/houseplan-editor-runtime.ts');
assert.match(mutant.patches[0].replace, /this\.host\._serverCfg/);
// #642: диалог уехал в свой модуль; сохранённый конфиг там — член узкого
// порта, и подмена кандидата им по-прежнему типизирована.
assert.equal(mutant.patches[0].file, 'src/optimize-plans-dialog.ts');
assert.match(mutant.patches[0].replace, /this\.port\.config\(\)/);
});
test('#486 panel registration, cleanup and read-only protections have mutation witnesses', () => {
+504
View File
@@ -0,0 +1,504 @@
// #642: диалог «Оптимизировать планы» — исполнением через test-build, а не
// текстом монолита. Сюда же переехали 11 утверждений `i18n.test.mjs` о
// разметке и тосте диалога (AC3) и гварды восьми мутантов реестра (AC4).
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import ts from 'typescript';
import {
OptimizePlansDialog, preflightDiagnostics, preflightVersionsDiffer,
} from '../test-build/optimize-plans-dialog.js';
import { formatLatticeShiftCm } from '../test-build/coordinate-canonicalization.js';
import { spacePhysicalGeometryFingerprint } from '../test-build/plan-geometry-preflight.js';
import { contentFingerprint } from '../test-build/visual-continuity.js';
const MODULE_SOURCE = readFileSync(new URL('../src/optimize-plans-dialog.ts', import.meta.url), 'utf8');
const BUILT_SOURCE = readFileSync(new URL('../test-build/optimize-plans-dialog.js', import.meta.url), 'utf8');
// ---------------------------------------------------------------- fixtures
const space = (id, title, x) => ({
id, title, view_box: [0, 0, 1, 1], cell_cm: 5,
rooms: [{ id: `${id}-room`, name: 'R', area: null, poly: [[0.1, 0.1], [x, 0.1], [x, 0.4], [0.1, 0.4]] }],
});
const report = (patch = {}) => ({
moved: 0, total: 0, rotated: 0, maxShift: 0, maxShiftCm: 0, maxSpace: '',
modelFrom: 10, modelTo: 10, migrated: 0, glowSpacesMigrated: 0, glowRoomsMigrated: 0,
canonicalized: 0, wallSegmentsMigrated: 0, roomDraftsMigrated: 0, roomDraftSegmentsMigrated: 0,
legacyZeroWallsMigrated: 0, wallsMerged: 0, spansMerged: 0, partitionsMerged: 0,
partitionsReconciled: 0, openingsRehosted: 0, wallsStraightened: 0, wallsStraightenSkipped: 0,
maxStraightenShiftCm: 0, maxStraightenSpace: '', coordsCanonicalized: 0,
latticeCoordinatesCanonicalized: 0, latticeCoordinatesFar: 0, latticeMaxShift: 0,
latticeMaxShiftCm: 0, latticeSpaces: [],
spaceRefsRemapped: 0, roomRefsRemapped: 0, positionsRemapped: 0, markersDetached: 0,
orphanRoomLabelsRemoved: 0, orphanDevicePositionsRemoved: 0, orphanGroupPositionsRemoved: 0,
liveMissingPositionsRemoved: 0, nestedRefsUnresolved: 0,
removedPositions: [], liveMissingPositions: [], unverifiedPositions: [],
...patch,
});
const okPreflight = (config) => ({
fingerprint: contentFingerprint(config), spaces: [], failures: [], ok: true,
});
const failedPreflight = (fingerprint = 'fp-1') => ({
fingerprint, spaces: [], ok: false,
failures: [
{ spaceId: 'a', displayName: 'Alpha', status: 'failed', reason: 'wall-exception', detail: 'Error' },
{ spaceId: 'b2', displayName: 'Second', status: 'failed', reason: 'floor-null' },
],
});
const dialogOf = (patch = {}) => {
const config = patch.config ?? { spaces: [space('a', 'Alpha', 0.5)], markers: [], settings: {} };
return {
report: report(), config, layout: {}, preflight: okPreflight(config),
cm: 0, where: '', changed: true, busy: false, removeLiveMissingPositions: false,
...patch,
};
};
/** Фейковый порт: ровно члены `OptimizePlansDialogPort`, каждый считает вызовы. */
function fakePort(overrides = {}) {
const calls = {
commit: [], checkGeometry: [], toasts: [], requestUpdate: 0, migration: [],
gesture: 0, reload: 0, referenceContext: [],
};
const state = {
dialog: null,
config: { spaces: [space('a', 'Alpha', 0.5)], markers: [], settings: {} },
integration: null,
};
const port = {
dialog: () => state.dialog,
setDialog: (next) => { state.dialog = next; },
t: (key, vars) => (vars ? `${key}${JSON.stringify(vars)}` : key),
hass: () => ({}),
config: () => state.config,
planReady: () => true,
layout: () => ({}),
integrationVersion: () => state.integration,
cardVersion: () => '9.9.9',
requestUpdate: () => { calls.requestUpdate += 1; },
showToast: (message) => { calls.toasts.push(message); },
errorText: (error) => String(error?.message ?? error),
checkGeometry: (config) => { calls.checkGeometry.push(config); return okPreflight(config); },
referenceContext: (removeLive) => { calls.referenceContext.push(removeLive); return { removeLiveMissingPositions: removeLive }; },
showMigrationBlocked: (error) => { calls.migration.push(error); },
clearGeometryGesture: () => { calls.gesture += 1; },
commit: async (config, layout) => { calls.commit.push([config, layout]); },
reloadAfterConflict: async () => { calls.reload += 1; },
...overrides,
};
return { port, calls, state, dialog: new OptimizePlansDialog(port) };
}
// ------------------------------------------------------ TemplateResult helpers
const isTemplate = (value) => !!value && typeof value === 'object'
&& Array.isArray(value.strings) && Array.isArray(value.values);
/** Сплющенная разметка: строки шаблона и значения, `nothing` — пусто. */
function flatten(value) {
if (value == null || value === false || typeof value === 'symbol' || typeof value === 'function') return '';
if (Array.isArray(value)) return value.map(flatten).join('');
if (isTemplate(value)) {
return value.strings.reduce((out, part, index) => (
out + part + (index < value.values.length ? flatten(value.values[index]) : '')
), '');
}
return String(value);
}
/** Обработчики `@event=${fn}` вместе с текстом перед ними — чтобы найти кнопку. */
function handlers(value, out = []) {
if (Array.isArray(value)) { value.forEach((item) => handlers(item, out)); return out; }
if (!isTemplate(value)) return out;
value.values.forEach((item, index) => {
const match = /@([\w-]+)=$/.exec(value.strings[index]);
// Текст от начала тега до обработчика: по нему кнопка и находится.
const before = value.strings.slice(0, index + 1).join('');
const tag = before.slice(before.lastIndexOf('<'));
if (match && typeof item === 'function') out.push({ event: match[1], before: tag, fn: item });
else handlers(item, out);
});
return out;
}
const handlerFor = (template, event, marker) => handlers(template)
.find((item) => item.event === event && (!marker || item.before.includes(marker)))?.fn;
/**
* `navigator` в Node — геттер без сеттера, `t.mock.property` его не
* восстанавливает. Подменяем дескриптор и возвращаем исходный сами.
*/
function withClipboard(t, clipboard) {
const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
Object.defineProperty(globalThis, 'navigator', { value: { clipboard }, configurable: true });
t.after(() => {
if (original) Object.defineProperty(globalThis, 'navigator', original);
else delete globalThis.navigator;
});
}
// ------------------------------------------------------------- pure helpers
test('#642 preflightDiagnostics: candidate hash, reason, detail, version, fixed clock', () => {
const candidate = { spaces: [space('a', 'Alpha', 0.6)] };
const now = new Date('2026-09-24T10:00:00.000Z');
const block = preflightDiagnostics(failedPreflight(), candidate, { cardVersion: '1.2.3', now });
assert.equal(block.kind, 'houseplan-optimize-preflight');
assert.equal(block.origin, 'runtime');
assert.equal(block.cardVersion, '1.2.3');
assert.equal(block.checkedAt, '2026-09-24T10:00:00.000Z');
assert.equal(block.preflightFingerprint, 'fp-1');
assert.deepEqual(block.failures.map((failure) => [failure.spaceId, failure.reason, failure.detail]), [
['a', 'wall-exception', 'Error'], ['b2', 'floor-null', null],
]);
assert.equal(block.failures[0].spaceGeometryFingerprint, spacePhysicalGeometryFingerprint(candidate.spaces[0]));
assert.equal(block.failures[1].spaceGeometryFingerprint, null, 'a space missing from the candidate has no hash');
// Смок передаёт кандидата не всегда; блок всё равно собирается.
assert.equal(preflightDiagnostics(failedPreflight(), undefined, { cardVersion: 'x', now }).failures[0]
.spaceGeometryFingerprint, null);
});
test('#642 preflightVersionsDiffer: only a known, different integration version advises an update', () => {
assert.equal(preflightVersionsDiffer(null, '1.0.0'), false);
assert.equal(preflightVersionsDiffer('', '1.0.0'), false);
assert.equal(preflightVersionsDiffer('1.0.0', '1.0.0'), false);
assert.equal(preflightVersionsDiffer('0.0.1-other', '1.0.0'), true);
});
// ------------------------------------------------------------------ preview
test('#642 preview: a failed migration reports once and opens nothing', () => {
const boom = new Error('wall model');
const { dialog, calls, state } = fakePort({ referenceContext: () => { throw boom; } });
dialog.preview(false);
assert.deepEqual(calls.migration, [boom]);
assert.equal(state.dialog, null);
});
test('#642 preview: nothing without a plan or a saved config', () => {
const noPlan = fakePort({ planReady: () => false });
noPlan.dialog.open();
assert.equal(noPlan.state.dialog, null);
const noConfig = fakePort();
noConfig.state.config = null;
noConfig.dialog.open();
assert.equal(noConfig.state.dialog, null);
assert.deepEqual(noConfig.calls.referenceContext, []);
});
test('#642 preview: cm is rounded UP, the space is named only when there are several', () => {
const two = fakePort();
two.state.config = { spaces: [space('a', 'Alpha', 0.5), space('b', 'Beta', 0.50337)], markers: [], settings: {} };
two.dialog.preview(false);
const d = two.state.dialog;
assert.equal(d.changed, true);
assert.equal(d.report.maxSpace, 'b');
assert.equal(d.cm, Math.ceil(d.report.maxShiftCm * 10) / 10);
assert.ok(d.cm >= d.report.maxShiftCm, 'the promise is never smaller than the deed');
assert.equal(d.where, 'Beta');
assert.equal(d.busy, false);
assert.equal(d.removeLiveMissingPositions, false);
assert.deepEqual(two.calls.checkGeometry, [d.config], 'a changed candidate is preflighted');
assert.deepEqual(two.calls.referenceContext, [false]);
const one = fakePort();
one.state.config = { spaces: [space('b', 'Beta', 0.50337)], markers: [], settings: {} };
one.dialog.preview(false);
assert.equal(one.state.dialog.where, '');
});
test('#642 toggleLivePositions re-previews with the flag flipped, only when live positions exist', () => {
const { dialog, state, calls } = fakePort();
state.dialog = dialogOf();
dialog.toggleLivePositions();
assert.deepEqual(calls.referenceContext, [], 'no live-missing positions — nothing to toggle');
state.dialog = dialogOf({ report: report({ liveMissingPositions: [{ id: 'm', name: 'Lamp', kind: 'device', spaceId: 'a' }] }) });
dialog.toggleLivePositions();
assert.deepEqual(calls.referenceContext, [true]);
assert.equal(state.dialog.removeLiveMissingPositions, true);
});
// --------------------------------------------------------------------- run
test('#642 open: preview never writes, and cancel closes without a write', async () => {
const { dialog, state, calls } = fakePort();
state.config = { spaces: [space('a', 'Alpha', 0.5), space('b', 'Beta', 0.50337)], markers: [], settings: {} };
dialog.open();
await Promise.resolve();
assert.ok(state.dialog?.changed && state.dialog.preflight?.ok, 'a writable preview is open');
assert.deepEqual(calls.commit, [], 'opening the preview must not persist anything');
handlerFor(dialog.render(), 'click', 'data-hp="dialog-cancel"')();
assert.equal(state.dialog, null);
assert.deepEqual(calls.commit, []);
});
test('#642 run: red preflight is a hard write barrier', async () => {
const { dialog, state, calls } = fakePort();
state.dialog = dialogOf({ preflight: failedPreflight() });
await dialog.run();
assert.deepEqual(calls.commit, []);
assert.equal(calls.gesture, 0);
state.dialog = dialogOf({ changed: false });
await dialog.run();
state.dialog = dialogOf({ busy: true });
await dialog.run();
assert.deepEqual(calls.commit, [], 'unchanged or busy dialogs do not write either');
});
test('#642 run: a stale fingerprint is rechecked, and a red recheck stops the write', async () => {
const red = failedPreflight('fresh');
const { dialog, state, calls } = fakePort({
checkGeometry: (config) => { calls.checkGeometry.push(config); return red; },
});
const warns = [];
const original = console.warn;
console.warn = (...args) => { warns.push(args); };
try {
state.dialog = dialogOf({ preflight: { ...okPreflight({}), fingerprint: 'stale' } });
const candidate = state.dialog.config;
await dialog.run();
assert.deepEqual(calls.checkGeometry, [candidate]);
assert.equal(state.dialog.preflight, red, 'the dialog now shows the fresh refusal');
assert.deepEqual(calls.commit, []);
assert.equal(warns.length, 1, 'the fresh refusal is dev-logged');
} finally {
console.warn = original;
}
});
test('#642 run: success writes the exact pair, closes, and toasts counts including lattice cleanup', async () => {
const seen = [];
const { dialog, state, calls } = fakePort({
commit: async (config, layout) => { seen.push(state.dialog?.busy); calls.commit.push([config, layout]); },
});
state.dialog = dialogOf({ report: report({ moved: 2, latticeCoordinatesCanonicalized: 7, orphanRoomLabelsRemoved: 1 }) });
const { config, layout } = state.dialog;
await dialog.run();
assert.equal(calls.gesture, 1);
assert.deepEqual(seen, [true], 'busy while the write is in flight');
assert.equal(calls.commit.length, 1);
assert.equal(calls.commit[0][0], config);
assert.equal(calls.commit[0][1], layout);
assert.deepEqual(calls.checkGeometry, [], 'an unchanged fingerprint is not rechecked');
assert.equal(state.dialog, null);
assert.deepEqual(calls.toasts, ['gs.align_done{"n":"2","m":"7","r":"1"}']);
});
test('#642 run: conflict reloads both stores; outdated client gets its own toast', async () => {
const conflict = fakePort({ commit: async () => { throw Object.assign(new Error('c'), { code: 'conflict' }); } });
conflict.state.dialog = dialogOf();
await conflict.dialog.run();
assert.equal(conflict.calls.reload, 1);
assert.equal(conflict.state.dialog.busy, false, 'the dialog stays open and usable');
assert.deepEqual(conflict.calls.toasts, ['toast.error{"err":"c"}']);
const outdated = fakePort({ commit: async () => { throw { code: 'wall_model_client_outdated' }; } });
outdated.state.dialog = dialogOf();
await outdated.dialog.run();
assert.equal(outdated.calls.reload, 0);
assert.deepEqual(outdated.calls.toasts, ['toast.wall_model_client_outdated']);
});
// -------------------------------------------------------------- diagnostics
test('#642 copyDiagnostics hashes the candidate, not the saved config', async (t) => {
const { dialog, state, calls } = fakePort();
const candidate = { spaces: [space('a', 'Alpha', 0.6)], markers: [], settings: {} };
state.config = { spaces: [space('a', 'Alpha', 0.5)], markers: [], settings: {} };
state.dialog = dialogOf({ config: candidate, preflight: failedPreflight() });
let copied = null;
withClipboard(t, { writeText: async (text) => { copied = text; } });
await dialog.copyDiagnostics();
const block = JSON.parse(copied);
assert.equal(block.failures[0].spaceGeometryFingerprint, spacePhysicalGeometryFingerprint(candidate.spaces[0]));
assert.notEqual(block.failures[0].spaceGeometryFingerprint, spacePhysicalGeometryFingerprint(state.config.spaces[0]));
assert.equal(block.cardVersion, '9.9.9');
assert.deepEqual(calls.toasts, ['gs.preflight_copied']);
assert.equal(dialog.clipboardFallback, null);
});
test('#642 copyDiagnostics: the inline fallback belongs to one dialog showing', async (t) => {
const { dialog, state, calls } = fakePort();
withClipboard(t, { writeText: async () => { throw new Error('denied'); } });
state.dialog = dialogOf({ preflight: failedPreflight() });
await dialog.copyDiagnostics();
assert.match(dialog.clipboardFallback, /houseplan-optimize-preflight/);
assert.equal(calls.requestUpdate, 1, 'the fallback is not @state — the module asks for the render');
assert.match(flatten(dialog.render()), /<pre[^>]*>\{\n {2}"kind": "houseplan-optimize-preflight"/);
// Закрытие настоящим обработчиком hp-close.
handlerFor(dialog.render(), 'hp-close')();
assert.equal(state.dialog, null);
assert.equal(dialog.clipboardFallback, null);
// Новый отказ — новый объект диалога — без JSON прошлого отказа.
state.dialog = dialogOf({ preflight: failedPreflight('fp-2') });
assert.equal(dialog.clipboardFallback, null);
assert.doesNotMatch(flatten(dialog.render()), /<pre/);
});
test('#642 copyDiagnostics does nothing for a green or missing preflight', async (t) => {
const { dialog, state, calls } = fakePort();
let writes = 0;
withClipboard(t, { writeText: async () => { writes += 1; } });
await dialog.copyDiagnostics();
state.dialog = dialogOf();
await dialog.copyDiagnostics();
assert.equal(writes, 0);
assert.deepEqual(calls.toasts, []);
});
test('#642 reportPreflightFailure logs one structured record per distinct fingerprint', () => {
const { dialog } = fakePort();
const warns = [];
const original = console.warn;
console.warn = (...args) => { warns.push(args); };
try {
dialog.reportPreflightFailure(failedPreflight('fp-1'), null);
dialog.reportPreflightFailure(failedPreflight('fp-1'), null);
dialog.reportPreflightFailure({ ...okPreflight({}), fingerprint: 'fp-ok' }, null);
assert.equal(warns.length, 1, 'dedup: the same fingerprint logs once, a green one never');
assert.equal(warns[0][0], '[houseplan] optimize preflight failed');
assert.equal(warns[0][1].kind, 'houseplan-optimize-preflight');
assert.equal(warns[0][1].failures[0].reason, 'wall-exception');
dialog.reportPreflightFailure(failedPreflight('fp-2'), null);
assert.equal(warns.length, 2);
} finally {
console.warn = original;
}
});
// ------------------------------------------------------------------ render
test('#642 render: changes — every counter, lattice lines and orphan report (i18n.test AC3)', () => {
const { dialog, state } = fakePort();
state.config = { spaces: [space('a', 'Alpha', 0.5), space('b', 'Beta', 0.5)], markers: [], settings: {} };
const r = report({
moved: 2, total: 9, coordsCanonicalized: 3, partitionsMerged: 5, migrated: 1, canonicalized: 4,
wallsMerged: 6, spansMerged: 8, partitionsReconciled: 2, openingsRehosted: 1,
legacyZeroWallsMigrated: 1, latticeCoordinatesCanonicalized: 4, latticeMaxShiftCm: 0.0123,
latticeSpaces: [{ space: 'Alpha', canonicalized: 4, far: 1 }],
orphanRoomLabelsRemoved: 1, orphanDevicePositionsRemoved: 2, orphanGroupPositionsRemoved: 0,
removedPositions: [{ id: 'dev1', kind: 'device', spaceId: 'gone' }],
wallsStraightened: 1, maxStraightenShiftCm: 1.23, maxStraightenSpace: 'b',
});
state.dialog = dialogOf({ report: r, cm: 1.5, where: 'Beta' });
const text = flatten(dialog.render());
assert.match(text, /gs\.align_count\{"n":"2","total":"9","cm":"1.5"\}/);
assert.match(text, /gs\.align_where\{"s":"Beta"\}/);
assert.match(text, /gs\.zero_walls_migrated\{"n":"1"\}/);
// p — шум координат, i — независимые стены (#229): счётчики показываются, а не копятся молча.
assert.match(text, /gs\.optimize_changes\{"m":"1","c":"4","p":"3","w":"6","s":"8","i":"5"\}/);
assert.match(text, /gs\.optimize_coincident_partitions\{"n":"2"\}/);
assert.match(text, /gs\.optimize_openings_rehosted\{"n":"1"\}/);
assert.ok(text.includes(`gs.optimize_lattice_summary{"n":"4","cm":"${formatLatticeShiftCm(0.0123)}"}`));
assert.match(text, /gs\.optimize_lattice_space\{"space":"Alpha","n":"4","far":"1"\}/);
assert.match(text, /gs\.optimize_walls_straightened\{"n":"1","cm":"1.3"\}/);
assert.match(text, /gs\.optimize_walls_straightened_where\{"s":"Beta"\}/);
assert.match(text, /gs\.optimize_orphans_removed\{"total":"3","rooms":"1","devices":"2","groups":"0"\}/);
assert.match(text, /<details class="optimize-details">/);
assert.match(text, /gs\.align_warn/);
assert.doesNotMatch(text, /gs\.optimize_reference_warning/);
assert.match(text, /data-hp="dialog-confirm"/);
});
test('#642 render: failed preflight names each reason and offers no Apply', () => {
const { dialog, state } = fakePort();
state.dialog = dialogOf({ preflight: failedPreflight() });
const text = flatten(dialog.render());
assert.match(text, /gs\.align_preflight_failed\{"spaces":"Alpha, Second","more":""\}/);
assert.match(text, /Alpha: gs\.preflight_reason_wall-exception/);
assert.match(text, /Second: gs\.preflight_reason_floor-null/);
assert.match(text, /gs\.preflight_copy/);
assert.doesNotMatch(text, /data-hp="dialog-confirm"/, 'the failure state is not a dismissible warning');
assert.doesNotMatch(text, /gs\.preflight_update_hint/);
state.integration = '0.0.1-other';
assert.match(flatten(dialog.render()), /gs\.preflight_update_hint/);
});
test('#642 render: nothing to do — plain «none» or «no automatic changes», never Apply', () => {
const { dialog, state } = fakePort();
state.dialog = dialogOf({ changed: false, preflight: null });
let text = flatten(dialog.render());
assert.match(text, /gs\.align_none/);
assert.doesNotMatch(text, /data-hp="dialog-confirm"/);
state.dialog = dialogOf({ changed: false, preflight: null, report: report({ nestedRefsUnresolved: 2 }) });
text = flatten(dialog.render());
assert.match(text, /gs\.optimize_no_automatic_changes/);
assert.match(text, /gs\.optimize_vacuum_warning\{"n":"2"\}/);
});
test('#642 render: live positions — names, toggle state and the selected hint', () => {
const { dialog, state, calls } = fakePort();
const live = ['A', 'B', 'C', 'D'].map((name, index) => ({ id: `m${index}`, name, kind: 'device', spaceId: 'a' }));
state.dialog = dialogOf({ report: report({ liveMissingPositions: live }) });
let text = flatten(dialog.render());
assert.ok(text.includes('gs.optimize_live_positions{"n":"4","names":"gs.optimize_live_names{\\"names\\":\\"A, B, C\\",\\"more\\":\\"gs.optimize_reference_more{\\\\\\"n\\\\\\":\\\\\\"1\\\\\\"}\\"}"}'));
assert.match(text, /aria-pressed=false/);
assert.match(text, /gs\.optimize_live_remove/);
assert.doesNotMatch(text, /gs\.optimize_live_selected/);
state.dialog = { ...state.dialog, removeLiveMissingPositions: true };
text = flatten(dialog.render());
assert.match(text, /gs\.optimize_live_positions_remove/);
assert.match(text, /aria-pressed=true/);
assert.match(text, /gs\.optimize_live_keep/);
assert.match(text, /gs\.optimize_live_selected/);
handlerFor(dialog.render(), 'click', 'optimize-cleanup')();
assert.deepEqual(calls.referenceContext, [false], 'the toggle re-previews with the flag flipped back');
});
test('#642 render: details list at most ten items, statuses by kind, and the remainder', () => {
const { dialog, state } = fakePort();
const removed = Array.from({ length: 8 }, (_, index) => ({ id: `r${index}`, kind: 'room_label', spaceId: 'x' }));
const unverified = Array.from({ length: 4 }, (_, index) => ({
id: `u${index}`, kind: index ? 'group' : 'mystery', spaceId: 'y', reason: 'registry_unavailable',
}));
state.dialog = dialogOf({ report: report({ removedPositions: removed, unverifiedPositions: unverified }) });
const text = flatten(dialog.render());
assert.equal(text.split('<li>').length - 1, 10);
assert.match(text, /gs\.optimize_detail_item\{"status":"gs\.optimize_detail_removed","kind":"gs\.optimize_detail_room_label","id":"r0","space":"x"\}/);
assert.match(text, /gs\.optimize_detail_item\{"status":"gs\.optimize_detail_unverified","kind":"gs\.optimize_detail_unknown","id":"u0","space":"y"\}/);
assert.match(text, /gs\.optimize_details_more\{"n":"2"\}/);
assert.match(text, /gs\.optimize_unverified\{"n":"4"\}/);
assert.match(text, /gs\.optimize_registry_limited/);
});
// ------------------------------------------------------------ AC2: the port
const parsedModule = ts.createSourceFile('optimize-plans-dialog.ts', MODULE_SOURCE, ts.ScriptTarget.Latest, true);
const portMembers = () => {
const port = parsedModule.statements.find((statement) => ts.isInterfaceDeclaration(statement)
&& statement.name.text === 'OptimizePlansDialogPort');
assert.ok(port, 'the module declares its own port');
return port.members.map((member) => member.name.getText(parsedModule)).sort();
};
test('#642 AC2: the dialog port is narrow and the module never reaches the editor host', () => {
// Импорты — и типовые тоже: `import type` стирается в сборке, поэтому
// разбирается исходник модуля, а собранный файл проверяется отдельно.
const specifiers = parsedModule.statements
.filter((statement) => ts.isImportDeclaration(statement))
.map((statement) => statement.moduleSpecifier.text);
const host = /\/houseplan-(?:card|editor-runtime)$/;
assert.deepEqual(specifiers.filter((specifier) => host.test(specifier)), []);
const identifiers = new Set();
const walk = (node) => {
if (ts.isIdentifier(node)) identifiers.add(node.text);
ts.forEachChild(node, walk);
};
walk(parsedModule);
assert.equal(identifiers.has('HouseplanEditorHostPort'), false, 'the wide editor port is not referenced');
assert.equal(identifiers.has('host'), false, 'no `host` field or parameter — only the narrow port');
assert.doesNotMatch(BUILT_SOURCE, /houseplan-(?:card|editor-runtime)\.js/);
const members = portMembers();
assert.ok(members.length <= 20, `port has ${members.length} members`);
const { port, dialog } = fakePort();
assert.ok(Object.keys(port).length <= 20);
assert.deepEqual(Object.keys(port).sort(), members, 'the fake port is exactly the declared port');
assert.equal('host' in dialog, false);
});
+1 -1
View File
@@ -33,7 +33,7 @@
"src/coordinate-canonicalization.ts",
"src/serialized-write-queue.ts",
"src/align-grid.ts",
"src/plan-optimizer.ts", "src/plan-optimize-write.ts", "src/space-reference-repair.ts", "src/space-deletion.ts", "src/space-copy.ts", "src/space-copy-runtime.ts",
"src/plan-optimizer.ts", "src/plan-optimize-write.ts", "src/optimize-plans-dialog.ts", "src/space-reference-repair.ts", "src/space-deletion.ts", "src/space-copy.ts", "src/space-copy-runtime.ts",
"src/plan-geometry-preflight.ts", "src/draft-live-preflight.ts", "src/draft-live-commit.ts",
"src/furniture.ts", "src/furniture-plan-art.generated.ts", "src/decor-assets.ts",
"src/floating-surface.ts", "src/floating-surface-controller.ts", "src/help-behavior.ts", "src/support-feedback.ts",