Монолит: мёртвый код снят по noUnusedLocals, связность измеряется шестью числами и гейтом (#624)

Карточка и редакторский рантайм держали ≈380 неиспользуемых импортов, 56
мёртвых объявлений и дублей типов (warm-boot, LS_*, GLOW_*, debounce,
navigate, lruRead — копии карточки в рантайме) и 112 приватных членов
карточки, которых не читал никто — делегаты `_editorRuntimeOrThrow()._x()`,
оставшиеся от выноса #425, и аксессоры glow-состояния. Всё это снято; в 9
других файлах — по одиночной ошибке. Делегаты и поля, которых касаются
браузерные смоки (`card._x(...)`), оставлены и посчитаны отдельно.

Гейт `npm run lint:unused` (scripts/unused-locals-gate.mjs, в gate:small и
Validate после сборки): `tsc --noUnusedLocals` чист, кроме приватных членов
карточки из порта HouseplanEditorHostPort / `host.` (portPrivates) и членов,
которых зовёт харнесс (harnessPrivates); храповик по шести числам
scripts/monolith-metrics.mjs против scripts/monolith-baseline.json —
delegates 260→159, portMembers 350, hostRefs 4948, portPrivates 96,
harnessPrivates 107, bundleBytes 2 510 141→2 500 387. `npm run inventory`
печатает те же числа. Заморозка 54 тестов, читающих монолит как текст
(test/monolith-text-anchors.test.mjs); PROCESS.md §2.7 — правило.

Логический исходник для контрактных тестов (test/houseplan-source.mjs)
дописывает члены рантайма без делегата в карточке — контракт продукта не
зависит от наличия заглушки. Потолки ядер и initial gzip опущены на выигрыш
(292 000 → 290 400). Бандл пересобран, три копии синхронны.

Issue: #624
User-Visible: no
This commit is contained in:
Claude
2026-09-23 21:09:44 +03:00
parent 14e56c0399
commit 47469bab22
75 changed files with 1344 additions and 1796 deletions
+5
View File
@@ -760,6 +760,11 @@ jobs:
run: |
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
npm run bundle:budget
# #624: `tsc --noUnusedLocals` чист вне порта/харнесса, шесть чисел
# связности монолита не выше scripts/monolith-baseline.json. После
# сборки — одно из чисел считает байты dist/.
- name: Мёртвый код и связность монолита
run: npm run lint:unused
# Бандл собирается один раз и уезжает артефактом (#336). Прежде каждая из
# пяти браузерных job собирала его заново: `bundle:sync` — это `tsc
# --noEmit` плюс rollup, около минуты на job.
+16
View File
@@ -263,6 +263,22 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
fix-up. Серверный стопор — шаг слияния конвейера сверяет вершину ветки с
SHA материала ревью (допустим только собственный doc-коммит публикации
поверх) и при расхождении отменяет слияние с возвратом в `S6-in-progress`.
- **Контракты по монолиту — исполнением, не regex по тексту (#624).**
Новое утверждение о `src/houseplan-card.ts` или
`src/houseplan-editor-runtime.ts` доказывается экспортом функции и её
вызовом в `test-build`, а не поиском строки в исходнике: текстовый якорь
краснеет на переносе метода без единой регрессии, и это делает вынос дороже,
чем оставить монолит как есть. Список тестов, читающих монолит как текст,
заморожен (`test/monolith-text-anchors.test.mjs`, 54 файла на 23.09.2026) и
может только уменьшаться; новое имя в нём — находка ревью, а не запись в
список. Связность монолита измеряется шестью числами
(`scripts/monolith-metrics.mjs`: делегаты, члены порта, `host.`, приватные
члены порта и харнесса, байты `dist/`), база — `scripts/monolith-baseline.json`;
гейт `npm run lint:unused` (в `gate:small` и Validate после сборки) красит
рост любого из них и любой мёртвый код по `noUnusedLocals` вне порта и
харнесса. Снижение фиксируется тем же коммитом
(`node scripts/unused-locals-gate.mjs --update`); рост — только с записью в
issue задачи и правкой базы в том же коммите.
- **Выход:** очередь на пре-релиз либо возврат в «В разработке», не более
4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10).
@@ -1,109 +1,109 @@
{
"schema": 1,
"fingerprint": "b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce",
"fingerprint": "69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 291016,
"initialViewGzipBytes": 289452,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 292537,
"initialPanelGzipBytes": 290976,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2325,
"initialPanelOnlyGzipBytes": 2327,
"lazyFiles": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/de-CZpcqQf7.js",
"houseplan-assets/editor-C3IC2GQJ.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/fr-CaLl1Wrq.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js",
"houseplan-assets/hp-zigbee-topology-overlay-C-Lu_oc7.js",
"houseplan-assets/iso-scene-render-CReOrPSg.js",
"houseplan-assets/live-interaction-runtime-CsFTHrW4.js",
"houseplan-assets/pdf-export-CrSsg6Pr.js",
"houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js",
"houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/de-BS9wHWi6.js",
"houseplan-assets/editor-BMZm1YSF.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/fr-BXTCaZ6b.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js",
"houseplan-assets/hp-zigbee-topology-overlay-DeghrMYb.js",
"houseplan-assets/iso-scene-render-WErCGfb7.js",
"houseplan-assets/live-interaction-runtime-DqZvf8N9.js",
"houseplan-assets/pdf-export-CRHFi1dw.js",
"houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js",
"houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js"
],
"lazyGzipBytes": 460952,
"lazyGzipBytes": 460939,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/editor-C3IC2GQJ.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/editor-BMZm1YSF.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js"
],
"lazyEditorGzipBytes": 244437,
"lazyEditorGzipBytes": 244421,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js"
],
"lazyOnboardingGzipBytes": 34525,
"lazyOnboardingGzipBytes": 34527,
"lazyLocaleFiles": [
"houseplan-assets/de-CZpcqQf7.js",
"houseplan-assets/fr-CaLl1Wrq.js"
"houseplan-assets/de-BS9wHWi6.js",
"houseplan-assets/fr-BXTCaZ6b.js"
],
"lazyLocaleGzipBytes": 55258,
"lazyLocaleGzipBytes": 55262,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-CReOrPSg.js"
"houseplan-assets/iso-scene-render-WErCGfb7.js"
],
"lazyIsometricGzipBytes": 17641,
"lazyIsometricGzipBytes": 17645,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js"
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js"
],
"lazyFurnitureArtGzipBytes": 16939,
"lazyFurnitureArtGzipBytes": 16937,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/pdf-export-CrSsg6Pr.js"
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/pdf-export-CRHFi1dw.js"
],
"lazyPdfGzipBytes": 126825,
"lazyPdfGzipBytes": 126824,
"files": [
{
"path": "houseplan-assets/backdrop-pick-D9K8tWdi.js",
"sha256": "4569fbec1b44637cc46fb92aa3bf985064608b4af5155a38f557d025c3aeecdc",
"path": "houseplan-assets/backdrop-pick-DySspgf9.js",
"sha256": "fd9a1e2c1740a2b60c8104f28b4bb4c47b3c28142d665bfae84cb33e453db8a3",
"rawBytes": 76524,
"gzipBytes": 24572,
"gzipBytes": 24574,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CZpcqQf7.js",
"sha256": "d9abf54308cb9b642249e3fee8f71162719dc499ba9b4d1b5e16d24aacc0fb3a",
"path": "houseplan-assets/de-BS9wHWi6.js",
"sha256": "1f92be7e8df667e76658bb886bf4f202f7d9c3ae18d02efdec6d736ce7251ad9",
"rawBytes": 92247,
"gzipBytes": 27866,
"gzipBytes": 27867,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-C3IC2GQJ.js",
"sha256": "b0880e3b3986e0424f8b0a8ebbf297a2b9ec45c8e1bcf7186b12b83087b1daa9",
"path": "houseplan-assets/editor-BMZm1YSF.js",
"sha256": "e1d7b66ad6a074ee18734017725b6a0e7073daa9679f73bf1092fec9c496742d",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-EA8ftmO7.js",
"sha256": "07463e624837fcaa7695d741feac5d05c1b6f71470b247ff804edce7ad52e79e",
"path": "houseplan-assets/form-kit.styles-BgmGBrDY.js",
"sha256": "c42cd77459cea989cbfb7ff3d4d34ae41571cbc0d04fc79d31eb8f29cd01aa35",
"rawBytes": 25600,
"gzipBytes": 5056,
"isEntry": false,
@@ -111,202 +111,202 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-CaLl1Wrq.js",
"sha256": "50850549b497303d1465ba42659c0328f3c06be1beca6ab76e977a0d019cf4de",
"path": "houseplan-assets/fr-BXTCaZ6b.js",
"sha256": "4be52d798269354b3dceb9f143b1589f1476a17a612a97a485d77c928a2a9ea5",
"rawBytes": 94579,
"gzipBytes": 27392,
"gzipBytes": 27395,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"sha256": "281f8e8637ea0e6336b6103856581334da41fb130d48929dfccaa34246abe97a",
"path": "houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"sha256": "746f7d9b34f3ab2391a119d4ff7949ae4c4a029767933c067760946e4b4f2dd3",
"rawBytes": 52035,
"gzipBytes": 16939,
"gzipBytes": 16937,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BY9itxO5.js",
"sha256": "c70a8eff7f2975152f9236b8dd05800eae665ce17e8fc8529196ea677220bac1",
"rawBytes": 1031784,
"gzipBytes": 290212,
"path": "houseplan-assets/houseplan-card-74_n9MKq.js",
"sha256": "a83510bc7fc6f01f53e237f75705fcb614e0eaa15755137b649990eb4526a93e",
"rawBytes": 1022199,
"gzipBytes": 288649,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CZpcqQf7.js",
"houseplan-assets/editor-C3IC2GQJ.js",
"houseplan-assets/fr-CaLl1Wrq.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js",
"houseplan-assets/hp-zigbee-topology-overlay-C-Lu_oc7.js",
"houseplan-assets/iso-scene-render-CReOrPSg.js",
"houseplan-assets/live-interaction-runtime-CsFTHrW4.js",
"houseplan-assets/pdf-export-CrSsg6Pr.js",
"houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js"
"houseplan-assets/de-BS9wHWi6.js",
"houseplan-assets/editor-BMZm1YSF.js",
"houseplan-assets/fr-BXTCaZ6b.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js",
"houseplan-assets/hp-zigbee-topology-overlay-DeghrMYb.js",
"houseplan-assets/iso-scene-render-WErCGfb7.js",
"houseplan-assets/live-interaction-runtime-DqZvf8N9.js",
"houseplan-assets/pdf-export-CRHFi1dw.js",
"houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"sha256": "38e4bfa090a4d2cdb1ccb5dae65c76768bd1286e946b3a56466446e1b42cefc9",
"rawBytes": 678284,
"gzipBytes": 188857,
"path": "houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"sha256": "6e23016bf9d6074118c32546177027da79e28ad8585fc908725e1af1798c3c74",
"rawBytes": 678211,
"gzipBytes": 188840,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js"
"houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js",
"sha256": "ddf7494bfa398197847ba2b96f90bfb39bc325bacbf22835027b7a2379ab87e8",
"path": "houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js",
"sha256": "3974377e68797bc186c4472f3c3f3157bedea7b51dcfd55a2728870ffb408730",
"rawBytes": 15986,
"gzipBytes": 4897,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-C-Lu_oc7.js",
"sha256": "097ff6397602f18c1ed69ab50fc58830e43eca4a1e40c99d3c827981aedde33b",
"path": "houseplan-assets/hp-zigbee-topology-overlay-DeghrMYb.js",
"sha256": "d54d3e5de2d468a686858650d0d53bd05244cd9a0b259dd887407fcc5acbdc60",
"rawBytes": 10759,
"gzipBytes": 3686,
"gzipBytes": 3688,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js",
"houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js"
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js",
"houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-CReOrPSg.js",
"sha256": "1e100c3c5bef97e44c16f201e41907eb028b00a66a3d27b65d1583884c813972",
"path": "houseplan-assets/iso-scene-render-WErCGfb7.js",
"sha256": "6772d50b89b5bd67a5474015f207ca2f38eca1a28e5ac60eef4fb69664fa5051",
"rawBytes": 51983,
"gzipBytes": 17641,
"gzipBytes": 17645,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-CsFTHrW4.js",
"sha256": "5d98b59a161dcf580ab5d85f8414b796e907bfb0ae659dac9bac91eb67101369",
"path": "houseplan-assets/live-interaction-runtime-DqZvf8N9.js",
"sha256": "73348aa79ee4844660967192325d8e92fc59bb22f58c16257ab6549cc27a866d",
"rawBytes": 8216,
"gzipBytes": 3207,
"gzipBytes": 3209,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CrSsg6Pr.js",
"sha256": "a7f83db027e80ea688a89ad7a09d3be6b3bb2e368f5c18bcc28972ad55aa5551",
"path": "houseplan-assets/pdf-export-CRHFi1dw.js",
"sha256": "024231a2788e3ced48ee5ebaed5fde4acfef30c0ebcad5a36e8e6ba31b482a63",
"rawBytes": 236264,
"gzipBytes": 109886,
"gzipBytes": 109887,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-BjEKe8V9.js",
"sha256": "6d74cfa11a4dc9bc3eb6f247123be6752041af446b2b152b18f6a56690c26854",
"path": "houseplan-assets/summary-panel-editor-D2YQTymt.js",
"sha256": "4a908021bb1a50ec6260ac5a70fd498354c22c8abd3b354eba06133f74f50f86",
"rawBytes": 14862,
"gzipBytes": 3423,
"gzipBytes": 3427,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js"
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-oJjTO9qs.js",
"sha256": "dabe7653b8518e78fcf704d0a2bff1b45aedd02e0bd3d49d3e428752639df8f1",
"path": "houseplan-assets/summary-panel-metrics-DSS3_nCF.js",
"sha256": "0ab33c934349bc4295eea08a0cd47a241fd2f38fb4762a9617358cb88bacf6b1",
"rawBytes": 2611,
"gzipBytes": 1306,
"gzipBytes": 1308,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js",
"sha256": "6d0b9bd9892311c677e5ec8ff0b3c7cc7bb239d92f244d298a8a0fa516dc66e3",
"rawBytes": 72733,
"gzipBytes": 20175,
"path": "houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js",
"sha256": "e59722f015963f810ff64e8801d475eaec6ac5569198e5ddec46ffa80d34206a",
"rawBytes": 72637,
"gzipBytes": 20166,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-BjEKe8V9.js",
"houseplan-assets/summary-panel-metrics-oJjTO9qs.js"
"houseplan-assets/summary-panel-editor-D2YQTymt.js",
"houseplan-assets/summary-panel-metrics-DSS3_nCF.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-BXiyBE_x.js",
"sha256": "a19c9de73ca251bdd25306bb8f99e87a6032531817239ed7f691167d58158f4e",
"path": "houseplan-assets/zigbee-topology-BgCwJt55.js",
"sha256": "5e8532d48a653b627e51d6f862426a523759749ffa6d7c7421a6bb5953117dce",
"rawBytes": 18962,
"gzipBytes": 7430,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js",
"sha256": "2e02c0c73b74f669c645555cb756b25bdd382a4d55d6176c4aa6c8923de55b64",
"path": "houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js",
"sha256": "da347518ff7635fb30b3eb9928161a479116e7af6b454dfa9d4edae161091b78",
"rawBytes": 3942,
"gzipBytes": 1765,
"gzipBytes": 1764,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js"
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "413b9549ebf0f0041ef99b54d18e4624fea808813eea8cf6b29405bdc9855aa9",
"sha256": "a7480e327b096cdb1743a7186fa135e102c1144d2fd99ed1c47953529d15c207",
"rawBytes": 1205,
"gzipBytes": 804,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "cc2b73659797ab651bef7cfeb54fcc99b2c6d9d29b904221fb6f40137653e438",
"sha256": "6c31e3fc5215353af80614b1dd84a0a6d7c464c0cca59ece499a63c1e35f48aa",
"rawBytes": 6165,
"gzipBytes": 2325,
"gzipBytes": 2327,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";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-BY9itxO5.js";import{c as C,f as M,C as L}from"./form-kit.styles-EA8ftmO7.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";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-74_n9MKq.js";import{c as C,f as M,C as L}from"./form-kit.styles-BgmGBrDY.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";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-BY9itxO5.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";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-74_n9MKq.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";import{c as a,A as e}from"./houseplan-card-BY9itxO5.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-DLY32mB5.js";import"./form-kit.styles-EA8ftmO7.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";import{c as a,A as e}from"./houseplan-card-74_n9MKq.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-BQYtTnkQ.js";import"./form-kit.styles-BgmGBrDY.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"
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";import{ek as e,N as t,a$ as n,L as i,ae as o,ac as r,bT as a,bS as s,a_ as l,aZ as c,b0 as u,el as d,em as f,bV as m,en as g}from"./houseplan-card-74_n9MKq.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=u(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),a=i.binding.slice(o+1),s="device"===r?e.registry.devices?.[a]:null,l="entity"===r?e.registry.entities?.[a]:null,c="device"===r?a:l?.device_id,u="device"===r?s?.area_id:l?.area_id||c&&e.registry.devices?.[c]?.area_id,d="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||u||"",f=d&&e.areaToSpace[d]||i.space||e.firstSpaceId;c&&e.registry.devices?.[c]&&e.spaceIds.has(f)&&(!n.devices.has(c)||"entity"===r&&n.liveEntities.has(a))&&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,u=_){try{let d=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=u(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const s=t(e);if(!s)continue;const l=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||s;v=y(v,a(l,m.physicalBodies)),yield}const _=m.cellCm/o;d+=s(v)*_*_/1e4}return d}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=d(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 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";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-BY9itxO5.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*h(c,l,d=_){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const 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 p(e,t,n=_){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function S(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 b(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,b as summaryEntityValue,S as summarySystemValue,p as totalCleanFloorAreaM2};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";import{n as t,b as n}from"./zigbee-topology-BXiyBE_x.js";import{aM as e}from"./houseplan-card-BY9itxO5.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};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";import{n as t,b as n}from"./zigbee-topology-BgCwJt55.js";import{aM as e}from"./houseplan-card-74_n9MKq.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};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";try{await import("./houseplan-assets/houseplan-card-BY9itxO5.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";try{await import("./houseplan-assets/houseplan-card-74_n9MKq.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
+144 -144
View File
@@ -1,109 +1,109 @@
{
"schema": 1,
"fingerprint": "b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce",
"fingerprint": "69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 291016,
"initialViewGzipBytes": 289452,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 292537,
"initialPanelGzipBytes": 290976,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2325,
"initialPanelOnlyGzipBytes": 2327,
"lazyFiles": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/de-CZpcqQf7.js",
"houseplan-assets/editor-C3IC2GQJ.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/fr-CaLl1Wrq.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js",
"houseplan-assets/hp-zigbee-topology-overlay-C-Lu_oc7.js",
"houseplan-assets/iso-scene-render-CReOrPSg.js",
"houseplan-assets/live-interaction-runtime-CsFTHrW4.js",
"houseplan-assets/pdf-export-CrSsg6Pr.js",
"houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js",
"houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/de-BS9wHWi6.js",
"houseplan-assets/editor-BMZm1YSF.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/fr-BXTCaZ6b.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js",
"houseplan-assets/hp-zigbee-topology-overlay-DeghrMYb.js",
"houseplan-assets/iso-scene-render-WErCGfb7.js",
"houseplan-assets/live-interaction-runtime-DqZvf8N9.js",
"houseplan-assets/pdf-export-CRHFi1dw.js",
"houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js",
"houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js"
],
"lazyGzipBytes": 460952,
"lazyGzipBytes": 460939,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/editor-C3IC2GQJ.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/editor-BMZm1YSF.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js"
],
"lazyEditorGzipBytes": 244437,
"lazyEditorGzipBytes": 244421,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js"
],
"lazyOnboardingGzipBytes": 34525,
"lazyOnboardingGzipBytes": 34527,
"lazyLocaleFiles": [
"houseplan-assets/de-CZpcqQf7.js",
"houseplan-assets/fr-CaLl1Wrq.js"
"houseplan-assets/de-BS9wHWi6.js",
"houseplan-assets/fr-BXTCaZ6b.js"
],
"lazyLocaleGzipBytes": 55258,
"lazyLocaleGzipBytes": 55262,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-CReOrPSg.js"
"houseplan-assets/iso-scene-render-WErCGfb7.js"
],
"lazyIsometricGzipBytes": 17641,
"lazyIsometricGzipBytes": 17645,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js"
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js"
],
"lazyFurnitureArtGzipBytes": 16939,
"lazyFurnitureArtGzipBytes": 16937,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/pdf-export-CrSsg6Pr.js"
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/pdf-export-CRHFi1dw.js"
],
"lazyPdfGzipBytes": 126825,
"lazyPdfGzipBytes": 126824,
"files": [
{
"path": "houseplan-assets/backdrop-pick-D9K8tWdi.js",
"sha256": "4569fbec1b44637cc46fb92aa3bf985064608b4af5155a38f557d025c3aeecdc",
"path": "houseplan-assets/backdrop-pick-DySspgf9.js",
"sha256": "fd9a1e2c1740a2b60c8104f28b4bb4c47b3c28142d665bfae84cb33e453db8a3",
"rawBytes": 76524,
"gzipBytes": 24572,
"gzipBytes": 24574,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-CZpcqQf7.js",
"sha256": "d9abf54308cb9b642249e3fee8f71162719dc499ba9b4d1b5e16d24aacc0fb3a",
"path": "houseplan-assets/de-BS9wHWi6.js",
"sha256": "1f92be7e8df667e76658bb886bf4f202f7d9c3ae18d02efdec6d736ce7251ad9",
"rawBytes": 92247,
"gzipBytes": 27866,
"gzipBytes": 27867,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-C3IC2GQJ.js",
"sha256": "b0880e3b3986e0424f8b0a8ebbf297a2b9ec45c8e1bcf7186b12b83087b1daa9",
"path": "houseplan-assets/editor-BMZm1YSF.js",
"sha256": "e1d7b66ad6a074ee18734017725b6a0e7073daa9679f73bf1092fec9c496742d",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-EA8ftmO7.js",
"sha256": "07463e624837fcaa7695d741feac5d05c1b6f71470b247ff804edce7ad52e79e",
"path": "houseplan-assets/form-kit.styles-BgmGBrDY.js",
"sha256": "c42cd77459cea989cbfb7ff3d4d34ae41571cbc0d04fc79d31eb8f29cd01aa35",
"rawBytes": 25600,
"gzipBytes": 5056,
"isEntry": false,
@@ -111,202 +111,202 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-CaLl1Wrq.js",
"sha256": "50850549b497303d1465ba42659c0328f3c06be1beca6ab76e977a0d019cf4de",
"path": "houseplan-assets/fr-BXTCaZ6b.js",
"sha256": "4be52d798269354b3dceb9f143b1589f1476a17a612a97a485d77c928a2a9ea5",
"rawBytes": 94579,
"gzipBytes": 27392,
"gzipBytes": 27395,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"sha256": "281f8e8637ea0e6336b6103856581334da41fb130d48929dfccaa34246abe97a",
"path": "houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"sha256": "746f7d9b34f3ab2391a119d4ff7949ae4c4a029767933c067760946e4b4f2dd3",
"rawBytes": 52035,
"gzipBytes": 16939,
"gzipBytes": 16937,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BY9itxO5.js",
"sha256": "c70a8eff7f2975152f9236b8dd05800eae665ce17e8fc8529196ea677220bac1",
"rawBytes": 1031784,
"gzipBytes": 290212,
"path": "houseplan-assets/houseplan-card-74_n9MKq.js",
"sha256": "a83510bc7fc6f01f53e237f75705fcb614e0eaa15755137b649990eb4526a93e",
"rawBytes": 1022199,
"gzipBytes": 288649,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-CZpcqQf7.js",
"houseplan-assets/editor-C3IC2GQJ.js",
"houseplan-assets/fr-CaLl1Wrq.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js",
"houseplan-assets/hp-zigbee-topology-overlay-C-Lu_oc7.js",
"houseplan-assets/iso-scene-render-CReOrPSg.js",
"houseplan-assets/live-interaction-runtime-CsFTHrW4.js",
"houseplan-assets/pdf-export-CrSsg6Pr.js",
"houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js"
"houseplan-assets/de-BS9wHWi6.js",
"houseplan-assets/editor-BMZm1YSF.js",
"houseplan-assets/fr-BXTCaZ6b.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js",
"houseplan-assets/hp-zigbee-topology-overlay-DeghrMYb.js",
"houseplan-assets/iso-scene-render-WErCGfb7.js",
"houseplan-assets/live-interaction-runtime-DqZvf8N9.js",
"houseplan-assets/pdf-export-CRHFi1dw.js",
"houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-Bu7qx-5V.js",
"sha256": "38e4bfa090a4d2cdb1ccb5dae65c76768bd1286e946b3a56466446e1b42cefc9",
"rawBytes": 678284,
"gzipBytes": 188857,
"path": "houseplan-assets/houseplan-editor-runtime-CGmHUET4.js",
"sha256": "6e23016bf9d6074118c32546177027da79e28ad8585fc908725e1af1798c3c74",
"rawBytes": 678211,
"gzipBytes": 188840,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js"
"houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DgQaWsRq.js",
"sha256": "ddf7494bfa398197847ba2b96f90bfb39bc325bacbf22835027b7a2379ab87e8",
"path": "houseplan-assets/houseplan-onboarding-runtime-CbQxbxm9.js",
"sha256": "3974377e68797bc186c4472f3c3f3157bedea7b51dcfd55a2728870ffb408730",
"rawBytes": 15986,
"gzipBytes": 4897,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-D9K8tWdi.js",
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/backdrop-pick-DySspgf9.js",
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-C-Lu_oc7.js",
"sha256": "097ff6397602f18c1ed69ab50fc58830e43eca4a1e40c99d3c827981aedde33b",
"path": "houseplan-assets/hp-zigbee-topology-overlay-DeghrMYb.js",
"sha256": "d54d3e5de2d468a686858650d0d53bd05244cd9a0b259dd887407fcc5acbdc60",
"rawBytes": 10759,
"gzipBytes": 3686,
"gzipBytes": 3688,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js",
"houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js"
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js",
"houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-CReOrPSg.js",
"sha256": "1e100c3c5bef97e44c16f201e41907eb028b00a66a3d27b65d1583884c813972",
"path": "houseplan-assets/iso-scene-render-WErCGfb7.js",
"sha256": "6772d50b89b5bd67a5474015f207ca2f38eca1a28e5ac60eef4fb69664fa5051",
"rawBytes": 51983,
"gzipBytes": 17641,
"gzipBytes": 17645,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-CsFTHrW4.js",
"sha256": "5d98b59a161dcf580ab5d85f8414b796e907bfb0ae659dac9bac91eb67101369",
"path": "houseplan-assets/live-interaction-runtime-DqZvf8N9.js",
"sha256": "73348aa79ee4844660967192325d8e92fc59bb22f58c16257ab6549cc27a866d",
"rawBytes": 8216,
"gzipBytes": 3207,
"gzipBytes": 3209,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CrSsg6Pr.js",
"sha256": "a7f83db027e80ea688a89ad7a09d3be6b3bb2e368f5c18bcc28972ad55aa5551",
"path": "houseplan-assets/pdf-export-CRHFi1dw.js",
"sha256": "024231a2788e3ced48ee5ebaed5fde4acfef30c0ebcad5a36e8e6ba31b482a63",
"rawBytes": 236264,
"gzipBytes": 109886,
"gzipBytes": 109887,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-CMri0Lbe.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/furniture-plan-art.generated-DobaZhEB.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-BjEKe8V9.js",
"sha256": "6d74cfa11a4dc9bc3eb6f247123be6752041af446b2b152b18f6a56690c26854",
"path": "houseplan-assets/summary-panel-editor-D2YQTymt.js",
"sha256": "4a908021bb1a50ec6260ac5a70fd498354c22c8abd3b354eba06133f74f50f86",
"rawBytes": 14862,
"gzipBytes": 3423,
"gzipBytes": 3427,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js"
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-oJjTO9qs.js",
"sha256": "dabe7653b8518e78fcf704d0a2bff1b45aedd02e0bd3d49d3e428752639df8f1",
"path": "houseplan-assets/summary-panel-metrics-DSS3_nCF.js",
"sha256": "0ab33c934349bc4295eea08a0cd47a241fd2f38fb4762a9617358cb88bacf6b1",
"rawBytes": 2611,
"gzipBytes": 1306,
"gzipBytes": 1308,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-DLY32mB5.js",
"sha256": "6d0b9bd9892311c677e5ec8ff0b3c7cc7bb239d92f244d298a8a0fa516dc66e3",
"rawBytes": 72733,
"gzipBytes": 20175,
"path": "houseplan-assets/summary-panel-runtime-loaded-BQYtTnkQ.js",
"sha256": "e59722f015963f810ff64e8801d475eaec6ac5569198e5ddec46ffa80d34206a",
"rawBytes": 72637,
"gzipBytes": 20166,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-EA8ftmO7.js",
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/form-kit.styles-BgmGBrDY.js",
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-BjEKe8V9.js",
"houseplan-assets/summary-panel-metrics-oJjTO9qs.js"
"houseplan-assets/summary-panel-editor-D2YQTymt.js",
"houseplan-assets/summary-panel-metrics-DSS3_nCF.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-BXiyBE_x.js",
"sha256": "a19c9de73ca251bdd25306bb8f99e87a6032531817239ed7f691167d58158f4e",
"path": "houseplan-assets/zigbee-topology-BgCwJt55.js",
"sha256": "5e8532d48a653b627e51d6f862426a523759749ffa6d7c7421a6bb5953117dce",
"rawBytes": 18962,
"gzipBytes": 7430,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-DNUVRNlJ.js",
"sha256": "2e02c0c73b74f669c645555cb756b25bdd382a4d55d6176c4aa6c8923de55b64",
"path": "houseplan-assets/zigbee-topology-runtime-BMoqqgmU.js",
"sha256": "da347518ff7635fb30b3eb9928161a479116e7af6b454dfa9d4edae161091b78",
"rawBytes": 3942,
"gzipBytes": 1765,
"gzipBytes": 1764,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js",
"houseplan-assets/zigbee-topology-BXiyBE_x.js"
"houseplan-assets/houseplan-card-74_n9MKq.js",
"houseplan-assets/zigbee-topology-BgCwJt55.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "413b9549ebf0f0041ef99b54d18e4624fea808813eea8cf6b29405bdc9855aa9",
"sha256": "a7480e327b096cdb1743a7186fa135e102c1144d2fd99ed1c47953529d15c207",
"rawBytes": 1205,
"gzipBytes": 804,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "cc2b73659797ab651bef7cfeb54fcc99b2c6d9d29b904221fb6f40137653e438",
"sha256": "6c31e3fc5215353af80614b1dd84a0a6d7c464c0cca59ece499a63c1e35f48aa",
"rawBytes": 6165,
"gzipBytes": 2325,
"gzipBytes": 2327,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BY9itxO5.js"
"houseplan-assets/houseplan-card-74_n9MKq.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";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-BY9itxO5.js";import{c as C,f as M,C as L}from"./form-kit.styles-EA8ftmO7.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";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-74_n9MKq.js";import{c as C,f as M,C as L}from"./form-kit.styles-BgmGBrDY.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";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-BY9itxO5.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";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-74_n9MKq.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
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__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";import{c as a,A as e}from"./houseplan-card-BY9itxO5.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-DLY32mB5.js";import"./form-kit.styles-EA8ftmO7.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";import{c as a,A as e}from"./houseplan-card-74_n9MKq.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-BQYtTnkQ.js";import"./form-kit.styles-BgmGBrDY.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"
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";import{ek as e,N as t,a$ as n,L as i,ae as o,ac as r,bT as a,bS as s,a_ as l,aZ as c,b0 as u,el as d,em as f,bV as m,en as g}from"./houseplan-card-74_n9MKq.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=u(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),a=i.binding.slice(o+1),s="device"===r?e.registry.devices?.[a]:null,l="entity"===r?e.registry.entities?.[a]:null,c="device"===r?a:l?.device_id,u="device"===r?s?.area_id:l?.area_id||c&&e.registry.devices?.[c]?.area_id,d="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||u||"",f=d&&e.areaToSpace[d]||i.space||e.firstSpaceId;c&&e.registry.devices?.[c]&&e.spaceIds.has(f)&&(!n.devices.has(c)||"entity"===r&&n.liveEntities.has(a))&&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,u=_){try{let d=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=u(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const s=t(e);if(!s)continue;const l=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||s;v=y(v,a(l,m.physicalBodies)),yield}const _=m.cellCm/o;d+=s(v)*_*_/1e4}return d}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=d(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 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";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-BY9itxO5.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*h(c,l,d=_){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const 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 p(e,t,n=_){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function S(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 b(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,b as summaryEntityValue,S as summarySystemValue,p as totalCleanFloorAreaM2};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";import{n as t,b as n}from"./zigbee-topology-BXiyBE_x.js";import{aM as e}from"./houseplan-card-BY9itxO5.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};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";import{n as t,b as n}from"./zigbee-topology-BgCwJt55.js";import{aM as e}from"./houseplan-card-74_n9MKq.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};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b9821d608f436104f6f390a7d9ca2821c0d3743a8ac9090e8606cdf9ed6a24ce";try{await import("./houseplan-assets/houseplan-card-BY9itxO5.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__="69203951e99e561d579810ce44a7a4baf1278315bb3708aedac1667a87eb8d97";try{await import("./houseplan-assets/houseplan-card-74_n9MKq.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
+1
View File
@@ -14,6 +14,7 @@
"gate:small": "node scripts/gate-small.mjs",
"toolchain:check": "node scripts/toolchain-pins.mjs --check",
"bundle:budget": "node scripts/bundle-budget.mjs",
"lint:unused": "node scripts/unused-locals-gate.mjs",
"invariants": "tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node scripts/model-invariants.mjs",
"docs:capture": "node scripts/assert-capture-env.mjs docs && npm run build && node demo/docs/capture.mjs",
"docs:accept": "node scripts/docs-accept.mjs",
+7 -1
View File
@@ -327,8 +327,14 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
* повторном открытии диалог теряет единую dirty-семантику до загрузки
* редактора. Факт оставляет 959 Б сверху и 1 041 Б до нижней границы;
* общий бюджет не меняется.
*
* 2026-09-23, #624: 292 000 → 290 400 (замер 289 448). Из карточки сняты
* мёртвые делегаты и поля, из обоих ядер — мёртвые копии объявлений и
* неиспользуемые импорты; rollup выбрасывал их и раньше, но не всё: минус
* 1 593 Б gzip в стартовом графе. Факт оставляет 952 Б сверху и 1 048 Б до
* нижней границы; общий бюджет не меняется.
*/
export const INITIAL_VIEW_GZIP_CEILING = 292_000;
export const INITIAL_VIEW_GZIP_CEILING = 290_400;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
+4 -3
View File
@@ -321,9 +321,10 @@ export const CHECKS = {
roots: ['docs/**', 'README.md', 'README.ru.md', 'PROCESS.md', 'AGENTS.md', 'CONTRIBUTING.md', ...WORKFLOW],
},
frontend: {
// npm run typecheck, npm test, npm run build, bundle:budget
entries: ['test/*.test.mjs', 'scripts/no-new-any.mjs', 'scripts/bundle-budget.mjs', 'scripts/fix-test-build.mjs'],
roots: [...BUILD_INPUTS, 'test/**', 'tsconfig*.json', ...WORKFLOW],
// npm run typecheck, npm test, npm run build, bundle:budget, lint:unused (#624)
entries: ['test/*.test.mjs', 'scripts/no-new-any.mjs', 'scripts/bundle-budget.mjs', 'scripts/fix-test-build.mjs',
'scripts/unused-locals-gate.mjs'],
roots: [...BUILD_INPUTS, 'test/**', 'tsconfig*.json', 'scripts/monolith-baseline.json', 'demo/smoke_*.mjs', 'demo/benchmark_*.mjs', 'demo/guard/*.mjs', ...WORKFLOW],
},
changed_mutants: {
entries: ['scripts/mutation-*.mjs', 'scripts/*-guard.mjs', 'test/*.test.mjs', 'demo/smoke_*.mjs', 'tests_backend/**/*.py'],
+3
View File
@@ -73,6 +73,9 @@ export function postBuildSteps() {
{ name: 'юниты (npm test)', cmd: npm, args: ['test'] },
{ name: 'копии бандла совпадают (bundle-tree)', cmd: process.execPath, args: ['scripts/bundle-tree.mjs', 'dist', 'custom_components/houseplan/frontend'], hint: 'npm run bundle:sync' },
{ name: 'бюджет бандла', cmd: npm, args: ['run', 'bundle:budget'] },
// #624: мёртвый код по noUnusedLocals и храповик связности монолита; после
// сборки, потому что одно из чисел — размер dist/.
{ name: 'мёртвый код и связность монолита (lint:unused)', cmd: process.execPath, args: ['scripts/unused-locals-gate.mjs'], hint: 'node scripts/unused-locals-gate.mjs --update при осознанном снижении' },
];
}
+15
View File
@@ -6,6 +6,7 @@
// underestimates the coverage that exists (review R5-2). The counts live here
// now, one command away, and STATUS.md describes the layers instead.
import { readdirSync, readFileSync } from 'node:fs';
import { METRIC_NAMES, collectMetrics, readBaseline } from './monolith-metrics.mjs';
const count = (dir, match, re) =>
readdirSync(dir)
@@ -25,3 +26,17 @@ const rows = [
];
const w = Math.max(...rows.map(([n]) => n.length));
for (const [name, n] of rows) console.log(`${name.padEnd(w)} ${n}`);
// #624: связность монолита — те же шесть чисел и тот же модуль, что у гейта
// `npm run lint:unused` («одно число — один источник»); рядом — база, чтобы
// движение было видно без git blame. bundleBytes есть только после сборки.
const { metrics } = collectMetrics(process.cwd());
const baseline = readBaseline(process.cwd()) || {};
console.log('\nmonolith (scripts/monolith-metrics.mjs; база scripts/monolith-baseline.json)');
const mw = Math.max(...METRIC_NAMES.map((n) => n.length));
for (const name of METRIC_NAMES) {
const now = metrics[name];
const base = baseline[name];
const delta = now != null && base != null && now !== base ? ` (${now > base ? '+' : ''}${now - base} к базе ${base})` : '';
console.log(`${name.padEnd(mw)} ${now ?? 'n/a — npm run build'}${delta}`);
}
+8
View File
@@ -0,0 +1,8 @@
{
"delegates": 159,
"portMembers": 350,
"hostRefs": 4948,
"portPrivates": 96,
"harnessPrivates": 107,
"bundleBytes": 2500387
}
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env node
/**
* Метрики связности монолита и мёртвый код в нём (#624).
*
* После выноса редакторского рантайма (#425) карточка и рантайм вместе стали
* длиннее на 22 %: карточка держит сотни однострочных делегатов
* `return this._editorRuntimeOrThrow()._x(...)`, рантайм читает её приватные
* поля через порт `HouseplanEditorHostPort` из 350+ членов, а `noUnusedLocals`
* был выключен — компилятор молчал о ≈380 неиспользуемых импортах и ≈60
* мёртвых объявлениях. Этот модуль — единственный источник шести чисел, по
* которым связность измеряется («одно число — один источник», PROCESS.md):
* их читают и гейт `scripts/unused-locals-gate.mjs`, и `npm run inventory`.
*
* delegates методов карточки, тело которых — один вызов рантайма
* portMembers членов интерфейса HouseplanEditorHostPort
* hostRefs обращений `host.` в src/** вне карточки
* portPrivates приватных членов карточки, которых карточка не читает,
* но читает рантайм или другой модуль через порт
* harnessPrivates приватных членов карточки, которых не читает никто в
* продукте, но зовут браузерные смоки (`card._x(...)`):
* мёртвый продуктовый код на службе харнесса
* bundleBytes байт в dist/ после сборки — сумма всех файлов, потому что
* `dist/houseplan-card.js` — загрузчик на 1 КБ, а код лежит
* в content-hashed чанках `dist/houseplan-assets/`; судится
* с полосой BUNDLE_BYTES_BAND, остальные — точно
*
* Приватный член карточки, который TypeScript считает непрочитанным, — не
* обязательно мёртвый: рантайм зовёт его как `host._x`. Поэтому гейт не
* включает `noUnusedLocals` в tsconfig (флаг нельзя ограничить «кроме членов
* порта», а объявить 300 членов публичными ради флага — спрятать метрику), а
* разбирает диагностики сам: разрешены поимённо только члены порта и члены,
* которых касается харнесс, и оба множества считаются.
*/
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import ts from 'typescript';
export const CARD_FILE = 'src/houseplan-card.ts';
export const RUNTIME_FILE = 'src/houseplan-editor-runtime.ts';
export const PORT_INTERFACE = 'HouseplanEditorHostPort';
export const RUNTIME_ACCESSOR = '_editorRuntimeOrThrow';
export const BASELINE_FILE = 'scripts/monolith-baseline.json';
export const METRIC_NAMES = ['delegates', 'portMembers', 'hostRefs', 'portPrivates', 'harnessPrivates', 'bundleBytes'];
/** Диагностики «объявлено, но не используется». */
export const UNUSED_CODES = new Set([6133, 6138, 6192, 6196, 6198, 6199]);
const parse = (name, text) => ts.createSourceFile(name, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const classNamed = (file, name) => file.statements.find(
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === name,
);
/**
* Делегат: метод/аксессор карточки, тело которого — ровно одно выражение
* `this._editorRuntimeOrThrow().<член>…` (с `return` или без, `async` или нет).
*/
export function isDelegateBody(body, sourceFile) {
if (!body || !ts.isBlock(body) || body.statements.length !== 1) return false;
const [statement] = body.statements;
let expression = null;
if (ts.isReturnStatement(statement)) expression = statement.expression ?? null;
else if (ts.isExpressionStatement(statement)) expression = statement.expression;
if (!expression) return false;
if (ts.isAwaitExpression(expression)) expression = expression.expression;
// this._editorRuntimeOrThrow()._x(...) | this._editorRuntimeOrThrow()._x
let node = expression;
while (ts.isCallExpression(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)
|| ts.isNonNullExpression(node) || ts.isAsExpression(node)) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
&& node.expression.name.text === RUNTIME_ACCESSOR && node.expression.expression.kind === ts.SyntaxKind.ThisKeyword) {
return node !== expression; // сам вызов аксессора без обращения к члену — не делегат
}
node = ts.isCallExpression(node) ? node.expression : node.expression;
}
void sourceFile;
return false;
}
/** Число делегатов в классе HouseplanCard. */
export function countDelegates(cardText) {
const file = parse(CARD_FILE, cardText);
const card = classNamed(file, 'HouseplanCard');
if (!card) throw new Error(`${CARD_FILE}: класс HouseplanCard не найден`);
let count = 0;
for (const member of card.members) {
if (ts.isMethodDeclaration(member) || ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) {
if (isDelegateBody(member.body, file)) count += 1;
}
}
return count;
}
/** Имена членов интерфейса HouseplanEditorHostPort. */
export function portMemberNames(runtimeText) {
const file = parse(RUNTIME_FILE, runtimeText);
const port = file.statements.find(
(statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === PORT_INTERFACE,
);
if (!port) throw new Error(`${RUNTIME_FILE}: интерфейс ${PORT_INTERFACE} не найден`);
const names = new Set();
for (const member of port.members) {
const name = member.name?.getText(file);
if (name) names.add(name.replace(/^['"]|['"]$/g, ''));
}
return names;
}
/** Обращения `host.<имя>` в исходниках вне карточки: общее число и имена. */
export function hostReferences(sourcesByPath) {
let count = 0;
const names = new Set();
for (const [path, text] of Object.entries(sourcesByPath)) {
if (path.replaceAll('\\', '/').endsWith(CARD_FILE)) continue;
for (const match of text.matchAll(/(?<![\w$])host\.([A-Za-z_$][\w$]*)/g)) {
count += 1;
names.add(match[1]);
}
}
return { count, names };
}
/** Имена, которых браузерные смоки касаются как свойств: `card._x(`, `c._x =`, `['_x']`. */
export function harnessMemberNames(demoSourcesByPath) {
const names = new Set();
for (const text of Object.values(demoSourcesByPath)) {
for (const match of text.matchAll(/(?:\.|\[['"])(_[A-Za-z][\w$]*)(?![\w$])/g)) names.add(match[1]);
}
return names;
}
/**
* Разбор диагностик компилятора с `noUnusedLocals`. Каждая — либо
* `allowed` (приватный член карточки из порта или харнесса), либо нарушение.
*/
export function classifyUnused(diagnostics, { cardText, portNames, hostNames, harnessNames }) {
const cardLines = cardText.split('\n');
const allowed = { port: [], harness: [] };
const violations = [];
for (const d of diagnostics) {
const name = /'([^']+)'/.exec(d.message)?.[1] ?? '';
const isCard = d.file.replaceAll('\\', '/').endsWith(CARD_FILE);
const lineText = isCard ? (cardLines[d.line - 1] ?? '') : '';
const isPrivateMember = /^\s+(?:@\w+(?:\([^)]*\))?\s+)?private\b/.test(lineText);
if (isCard && isPrivateMember && name) {
if (portNames.has(name) || hostNames.has(name)) { allowed.port.push(name); continue; }
if (harnessNames.has(name)) { allowed.harness.push(name); continue; }
}
violations.push({ ...d, name });
}
return { allowed, violations };
}
/** Суммарный размер файлов dist/ в байтах; null — если сборки нет. */
export function bundleBytes(distDir) {
if (!existsSync(distDir)) return null;
let total = 0;
const walk = (dir) => {
for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
const stat = statSync(path);
if (stat.isDirectory()) walk(path); else total += stat.size;
}
};
walk(distDir);
return total;
}
/** Диагностики `noUnusedLocals` по tsconfig проекта, в плоском виде. */
export function unusedDiagnostics(root, tsconfig = 'tsconfig.json') {
const configPath = resolve(root, tsconfig);
const config = ts.readConfigFile(configPath, ts.sys.readFile);
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'));
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root);
const program = ts.createProgram(parsed.fileNames, { ...parsed.options, noUnusedLocals: true, noEmit: true });
const out = [];
for (const d of ts.getPreEmitDiagnostics(program)) {
if (!UNUSED_CODES.has(d.code) || !d.file) continue;
const { line } = d.file.getLineAndCharacterOfPosition(d.start ?? 0);
out.push({
file: relative(root, d.file.fileName).replaceAll('\\', '/'),
line: line + 1,
code: d.code,
message: ts.flattenDiagnosticMessageText(d.messageText, ' '),
});
}
return out;
}
/** Все .ts под src/ — путь → текст. */
export function readSources(root, dir = 'src') {
const out = {};
const walk = (d) => {
for (const entry of readdirSync(d)) {
const path = join(d, entry);
if (statSync(path).isDirectory()) walk(path);
else if (/\.ts$/.test(entry) && !/\.d\.ts$/.test(entry)) out[relative(root, path).replaceAll('\\', '/')] = readFileSync(path, 'utf8');
}
};
walk(resolve(root, dir));
return out;
}
/**
* Харнесс — то, что исполняет Validate: смоки `demo/smoke_*.mjs`, бенчмарки
* и `demo/guard`. Ручные съёмки (`shot_*`, `verify_*`) входом не считаются
* (см. NOT_AN_INPUT в check-inputs.mjs) и мёртвый член карточки не оправдывают.
*/
export const HARNESS_FILE = /^(?:smoke_|benchmark_).*\.mjs$/;
export function readDemoSources(root) {
const out = {};
const demo = resolve(root, 'demo');
if (!existsSync(demo)) return out;
for (const entry of readdirSync(demo)) {
const path = join(demo, entry);
if (statSync(path).isFile() && HARNESS_FILE.test(entry)) out[`demo/${entry}`] = readFileSync(path, 'utf8');
}
const guard = join(demo, 'guard');
if (existsSync(guard)) {
for (const entry of readdirSync(guard)) {
if (/\.mjs$/.test(entry)) out[`demo/guard/${entry}`] = readFileSync(join(guard, entry), 'utf8');
}
}
return out;
}
/**
* Полный замер: шесть чисел плюс разбор диагностик. `diagnostics` можно
* подать снаружи (тесты), иначе компилятор запускается по tsconfig.
*/
export function collectMetrics(root, { diagnostics = null, distDir = 'dist' } = {}) {
const sources = readSources(root);
const cardText = sources[CARD_FILE];
const runtimeText = sources[RUNTIME_FILE];
if (!cardText || !runtimeText) throw new Error('карточка или рантайм не найдены в src/');
const portNames = portMemberNames(runtimeText);
const host = hostReferences(sources);
const harnessNames = harnessMemberNames(readDemoSources(root));
const diags = diagnostics ?? unusedDiagnostics(root);
const { allowed, violations } = classifyUnused(diags, { cardText, portNames, hostNames: host.names, harnessNames });
return {
metrics: {
delegates: countDelegates(cardText),
portMembers: portNames.size,
hostRefs: host.count,
portPrivates: allowed.port.length,
harnessPrivates: allowed.harness.length,
bundleBytes: bundleBytes(resolve(root, distDir)),
},
allowed,
violations,
};
}
/**
* Полоса для `bundleBytes` — как у gzip-потолка стартового графа (#438,
* `bundle-budget.mjs`): размер dist/ меняется от любой правки `src/**` в
* любой задаче, а ребейз ветки конвейером пересобирает бандл поверх нового
* `dev`. Точное число красило бы каждую ветку за чужой коммит. Пять чисел
* исходника такой полосы не получают: они меняются только правкой монолита.
*/
export const BUNDLE_BYTES_BAND = 2_000;
/** Сравнение с базой: что выросло, что упало. Отсутствующее в базе — рост. */
export function compareWithBaseline(current, baseline, { bundleBand = BUNDLE_BYTES_BAND } = {}) {
const grown = [];
const shrunk = [];
for (const name of METRIC_NAMES) {
const now = current[name];
const base = baseline?.[name];
if (now == null) continue;
const band = name === 'bundleBytes' ? bundleBand : 0;
if (base == null || now > base + band) grown.push({ name, base: base ?? null, now });
else if (now < base - band) shrunk.push({ name, base, now });
}
return { grown, shrunk };
}
export function readBaseline(root) {
const path = resolve(root, BASELINE_FILE);
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
}
export function formatMetrics(metrics) {
return METRIC_NAMES.map((name) => `${name}=${metrics[name] ?? 'n/a'}`).join(' ');
}
+44
View File
@@ -8605,6 +8605,50 @@ const MUTANT_DEFINITIONS = [
replace: " current = [line]; // mutant: last physical line instead of the paragraph\n }\n for (const paragraph of paragraphs) {",
}],
},
// #624: гейт мёртвого кода и связности монолита — три защиты, три мутанта.
{
id: 'unused-gate-allows-everything',
guard: 'node --test --test-name-pattern="#624 разбор диагностик" test/monolith-metrics.test.mjs',
because: 'the allow-list must admit only private card members read through the port or the '
+ 'smoke harness; admitting everything hides real dead code behind a green gate (#624 AC1)',
patches: [{
file: 'scripts/monolith-metrics.mjs',
find: " if (isCard && isPrivateMember && name) {",
replace: " if (name) { // mutant: any named diagnostic is allowed",
}],
},
{
id: 'monolith-metrics-baseline-strict',
guard: 'node --test --test-name-pattern="#624 храповик" test/monolith-metrics.test.mjs',
because: 'the ratchet reddens on a growth of one; a >= comparison lets bundle bytes and '
+ 'coupling creep up one unit per commit (#624 AC1-b, AC2)',
patches: [{
file: 'scripts/monolith-metrics.mjs',
find: " if (base == null || now > base + band) grown.push({ name, base: base ?? null, now });",
replace: " if (base == null || now > base + band + 1) grown.push({ name, base: base ?? null, now }); // mutant: one unit of slack",
}],
},
{
id: 'monolith-delegates-return-only',
guard: 'node --test --test-name-pattern="#624 делегаты" test/monolith-metrics.test.mjs',
because: 'a delegate without `return` or with `await` is still a delegate; counting only the '
+ 'return form undercounts and lets the metric fall without any extraction (#624 AC2)',
patches: [{
file: 'scripts/monolith-metrics.mjs',
find: " else if (ts.isExpressionStatement(statement)) expression = statement.expression;",
replace: " else if (ts.isExpressionStatement(statement)) return false; // mutant: return-form only",
}],
},
{
id: 'monolith-anchors-length-only',
guard: 'node --test --test-name-pattern="#624 AC4: подмена" test/monolith-text-anchors.test.mjs',
because: 'the freeze compares names, not counts: a renamed anchor test must read as a new one (#624 AC4)',
patches: [{
file: 'test/monolith-text-anchors.test.mjs',
find: " return actual.filter((name) => !allowed.has(name)).sort();",
replace: " return actual.length > frozen.length ? actual.slice(frozen.length).sort() : []; // mutant: length only",
}],
},
{
id: 'process-reconcile-restarts-healthy-run',
guard: 'node --test test/process-reconcile.test.mjs',
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Гейт «мёртвый код и связность монолита» (#624): `npm run lint:unused`.
*
* node scripts/unused-locals-gate.mjs # проверить против базы
* node scripts/unused-locals-gate.mjs --update # записать текущие числа в базу
* node scripts/unused-locals-gate.mjs --json # числа и разбор в JSON
*
* Две проверки, оба ответа — в одном прогоне:
*
* 1. `tsc --noUnusedLocals` по всему проекту чист, кроме приватных членов
* карточки, которых карточка не читает, но читает рантайм через порт
* (`portPrivates`) или зовут браузерные смоки (`harnessPrivates`). Любая
* другая диагностика — неиспользуемый импорт, мёртвая константа, локальная
* переменная, приватный член вне порта и харнесса — красный: это код,
* который компилятор доказал мёртвым.
*
* 2. Храповик: шесть чисел `scripts/monolith-metrics.mjs` не растут
* относительно `scripts/monolith-baseline.json` (`bundleBytes` — с полосой
* ±2 000 Б, как gzip-потолок #438; остальные — точно). Снижение — не ошибка, но
* база обязана быть опущена тем же коммитом (`--update`): незафиксированный
* выигрыш монолит отыграет обратно первой же правкой. Рост допускается
* только с явной записью в issue задачи и правкой базы в том же коммите —
* гейт печатает, какое число и на сколько.
*
* `bundleBytes` требует собранного `dist/`: гейт стоит после `npm run build`
* (в `gate:small` и в job `frontend` Validate). Без сборки число не судится,
* и гейт говорит об этом явно, а не молчит зелёным.
*/
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { isMainModule } from './spawn-portable.mjs';
import {
BASELINE_FILE, METRIC_NAMES, collectMetrics, compareWithBaseline, formatMetrics, readBaseline,
} from './monolith-metrics.mjs';
/**
* Чистое решение гейта: код выхода и строки отчёта по замеру и базе.
* `violations` — диагностики вне разрешённых; `grown` — выросшие числа.
*/
export function decide({ metrics, violations, baseline }) {
const lines = [];
let fail = false;
lines.push(`monolith: ${formatMetrics(metrics)}`);
if (violations.length) {
fail = true;
lines.push(`FAIL мёртвый код — ${violations.length} диагностик(и) noUnusedLocals вне порта и харнесса:`);
for (const v of violations.slice(0, 40)) lines.push(` ${v.file}:${v.line} TS${v.code} ${v.message}`);
if (violations.length > 40) lines.push(` … ещё ${violations.length - 40}`);
} else {
lines.push('ok мёртвого кода нет: единственные непрочитанные члены — порт и харнесс');
}
if (!baseline) {
fail = true;
lines.push(`FAIL базы ${BASELINE_FILE} нет — создать: node scripts/unused-locals-gate.mjs --update`);
return { fail, lines, grown: [], shrunk: [] };
}
const { grown, shrunk } = compareWithBaseline(metrics, baseline);
if (grown.length) {
fail = true;
for (const g of grown) {
lines.push(`FAIL связность выросла: ${g.name} ${g.base ?? 'нет в базе'} → ${g.now}`
+ ' — вернуть или обосновать в issue и поднять базу тем же коммитом');
}
}
if (shrunk.length) {
// Снижение без записи в базу — тоже отказ: храповик работает в обе стороны.
fail = true;
for (const s of shrunk) lines.push(`FAIL связность упала, база не опущена: ${s.name} ${s.base} → ${s.now} — node scripts/unused-locals-gate.mjs --update`);
}
if (!grown.length && !shrunk.length) lines.push('ok все числа равны базе');
if (metrics.bundleBytes == null) {
fail = true;
lines.push('FAIL dist/ не собран — bundleBytes не судится; сначала npm run build');
}
return { fail, lines, grown, shrunk };
}
export function baselineFrom(metrics) {
const out = {};
for (const name of METRIC_NAMES) out[name] = metrics[name];
return out;
}
if (isMainModule(import.meta.url)) {
const root = process.cwd();
const update = process.argv.includes('--update');
const json = process.argv.includes('--json');
const result = collectMetrics(root);
if (update) {
if (result.metrics.bundleBytes == null) {
console.error('dist/ не собран — база без bundleBytes была бы ложью; сначала npm run build');
process.exit(1);
}
writeFileSync(resolve(root, BASELINE_FILE), `${JSON.stringify(baselineFrom(result.metrics), null, 2)}\n`);
console.log(`${BASELINE_FILE} записан: ${formatMetrics(result.metrics)}`);
}
const baseline = readBaseline(root);
const decision = decide({ metrics: result.metrics, violations: result.violations, baseline });
if (json) {
console.log(JSON.stringify({ metrics: result.metrics, allowed: result.allowed, violations: result.violations, ...decision }, null, 2));
} else {
for (const line of decision.lines) console.log(line);
}
process.exit(decision.fail && !update ? 1 : 0);
}
+2 -2
View File
@@ -14,8 +14,8 @@
import { html, nothing, type TemplateResult } from 'lit';
import {
callout, chip, colorField, ensureFormKitStyles, field, fieldGrid, footerStatus, formCard, rangeEnds, rangeLine,
segmented, sourcePicker, subsection, textLink, tintBlock, toggleRow, unitInput,
callout, colorField, ensureFormKitStyles, field, fieldGrid, footerStatus, formCard, rangeEnds,
rangeLine, segmented, sourcePicker, subsection, textLink, tintBlock, toggleRow, unitInput,
} from './form-kit';
import { forgetMarkerBaseline, markerDirty, markerProblems } from './marker-form-state';
import { settingsCopy } from './settings-copy';
+62 -846
View File
File diff suppressed because it is too large Load Diff
+61 -461
View File
@@ -6,7 +6,7 @@
* 2) LEGACY fallback — baked-in country-house data (src/data/*), coordinates in a 1489×1053 canvas.
* The icon layout is stored on the server (houseplan/layout/*), fallback — localStorage.
*/
import { LitElement, html, svg, nothing, TemplateResult, PropertyValues } from 'lit';
import { html, svg, nothing, TemplateResult } from 'lit';
import { displayVersion } from './card-version';
import { guard } from 'lit/directives/guard.js';
import { renderVacuumMapsSection } from './editors/vacuum-maps-section';
@@ -17,7 +17,6 @@ import {
import { renderGeneralSettingsDialog } from './editors/general-settings-dialog';
import { renderRoomSettingsDialog } from './editors/room-settings-dialog';
import { calibrationTarget, planVacuumFit } from './vacuum-route-edit';
import { repeat } from 'lit/directives/repeat.js';
import {
cancelHouseplanPointerMove, flushHouseplanPointerMove, queueHouseplanPointerMove,
} from './pointer-move-queue';
@@ -39,25 +38,16 @@ import {
type IconRule, type CompiledIconRule,
} from './rules';
import {
lqiColor, snapToGrid, snapSegment45, samePoint, pointInPolygon, markerIdForBinding,
segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointStrictlyInside, roomsOverlap,
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, isExact45Vector, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, roomGlowOf, contentUrl,
snapToWall, snapPointAlongPoly, openingAmount, openingShoulders, interiorPoint,
isInteriorLightOpeningType, openingEntityReferences, filterOpeningEntityCandidates,
poleOfInaccessibility, subst,
averageLqi, fitView, declump, safeUrl, floorsOf, type FloorInfo,
stateIcon, lightColorOf, parseRoomRef, diffNewDevices, resolveGlowValues, resolveGlowAppearance,
glowAlpha, normalizeGlowColorOverride, isControllable,
spaceDisplayOf, resolveEffectiveRoomFill, fillColorsOf, DEFAULT_FILL_COLORS,
customFillOf, roomCustomFillOf, DEFAULT_CUSTOM_FILL,
type FillColors, type FillColorEntry, type ResolvedRoomFill, runServiceFor, RUN_TARGET_DOMAINS,
DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf, showRoomTooltipOf,
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
referencedContentUrls,
DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_UI_MODES, ROOM_FILL_MODES,
normalizeDeviceDisplay, isAlarmCapable, displayIsNeutral, displayWantsValue, type DeviceDisplayMode,
liveText, liveTextReference, liveTextToken, hassValue, valueWithUnit, decorTextScale, decorTextLines,
DECOR_TEXT_BASE,
snapToGrid, snapSegment45, samePoint, pointInPolygon, markerIdForBinding, formatLength, roomPoly,
pointStrictlyInside, roomsOverlap, pointOnBoundary, mergeRooms, splitRoomPath, polygonArea,
closestPointOnBoundary, pointStrictlyInside as ptInside, distToSegment, cutSegments, alignGuides,
segmentAngle, type AlignGuide, clampScale, migratePdfUrls, snapToWall, snapPointAlongPoly,
openingAmount, openingShoulders, filterOpeningEntityCandidates, poleOfInaccessibility, subst,
type FloorInfo, parseRoomRef, resolveGlowValues, normalizeGlowColorOverride, isControllable,
spaceDisplayOf, DEFAULT_FILL_COLORS, customFillOf, DEFAULT_CUSTOM_FILL, type FillColors,
type FillColorEntry, RUN_TARGET_DOMAINS, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf,
showRoomTooltipOf, DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, normalizeDeviceDisplay,
type DeviceDisplayMode, liveTextReference, liveTextToken, DECOR_TEXT_BASE,
} from './logic';
import {
resolveSafeResize,
@@ -79,13 +69,7 @@ import {
placeResizeAreaLabel, resizeMeasuredEdges,
type ResizeAreaPlacement,
} from './resize-labels';
import {
computeSunRays, dayPhase, northDegOf, bgModeOf, sunRaysOn, sunRayOriginOf,
sunStateOf, rayPeakAlpha, raysVisible, rayColor, RAY_FADE_MS, type SunRay,
rayStops, resolveDayCycle, dayCycleFingerprint, type DayCycleState,
rayRimEdges, rimStops, rimPeakAlpha, RIM_COLOR, type SunRayOrigin,
} from './sun';
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
import { northDegOf, bgModeOf, sunRaysOn, sunRayOriginOf, type SunRayOrigin } from './sun';
import {
furnitureDefaultCm,
furnitureGraphic, furnitureCorners,
@@ -100,24 +84,14 @@ import { FURNITURE_ART_RUNTIME } from './furniture-art-runtime';
import { FURNITURE_ART_FINGERPRINT, GENERATED_FURNITURE_ART } from './furniture-plan-art.generated';
import { furnitureWallSurfacesFor, type FurnitureWallSurface } from './furniture-wall-surface';
import {
degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations,
setWallThickness, setWallThicknessForRoom, cmToField, wallCmToUnits,
multiWallNodesForGeometry,
wallEdgeBodies, wallBodiesGeometry, wallBodiesGeometryPath, wallBodiesUnionPath,
recutWallBodiesGeometry,
floorFootprintGeometry,
innerContourForRoom, roomWallProfile, outsetContour,
openingInnerFaceOffsetFromIndex, openingTunnelGeometriesFromIndex,
openingWallIndex as buildOpeningWallIndex, resolveOpeningWallAssociation,
applyWallThicknessToNewRoom,
drawWallPreviewD, linearWallJoinPatches, DRAW_WALL_DEFAULT_CM,
wallIntervals, materializeWallIntervals,
normalizeWallIntervals,
intervalCmAt, wallBodyNeedsSolid, wallHatchNeedsSolid, wallHatchStepUnits,
HATCH_BASE_STEP_UNITS, type OpeningTunnelGeometry, type OpeningWallIndex,
type LinearWallSegment, type WallEntry, type WallInterval,
type MultiWallNodeMap,
innerEdgeSpan, ownEdgeOffsets, thicknessCmAt,
degradeWalls, rekeyWallsAfterMoveChecked, wallRecordCarrierViolations, setWallThickness,
setWallThicknessForRoom, cmToField, wallCmToUnits, multiWallNodesForGeometry, wallBodiesGeometry,
wallBodiesGeometryPath, wallBodiesUnionPath, innerContourForRoom,
openingInnerFaceOffsetFromIndex, resolveOpeningWallAssociation, applyWallThicknessToNewRoom,
drawWallPreviewD, linearWallJoinPatches, DRAW_WALL_DEFAULT_CM, wallIntervals,
materializeWallIntervals, intervalCmAt, wallHatchStepUnits, HATCH_BASE_STEP_UNITS,
type OpeningWallIndex, type LinearWallSegment, type WallEntry, type WallInterval,
type MultiWallNodeMap, innerEdgeSpan, ownEdgeOffsets, thicknessCmAt,
} from './wall-thickness';
import {
junctionLimitViolations, increasedViolations,
@@ -128,96 +102,60 @@ import {
type OpenSpanEntry,
} from './open-spans';
import { ContentSigner } from './signing';
import {
resolveFixedFloor, resolveInitialSpace, settleBestEffort,
type FixedFloorSelection, type InitialSpaceSelection,
} from './initial-load';
import { selectActiveSpaceModel, selectSpaceModelById } from './space-model-selection';
import { type InitialSpaceSelection } from './initial-load';
import { selectSpaceModelById } from './space-model-selection';
import {
applyRoomTempThresholdDraft, createEmptySpaceConfig, initialSpaceDisplayDraft,
roomTempThresholdDraft, roomTempThresholdInputValues, strictNumber, switchSpacePlanSource,
touchSpaceDisplay, type SpaceDialogState,
roomTempThresholdDraft, roomTempThresholdInputValues, strictNumber, type SpaceDialogState,
} from './space-dialog';
import { rememberSpaceDialogBaseline, spaceDialogProblems } from './editors/space-form-state';
import { generalProblems, rememberGeneralBaseline } from './editors/general-form-state';
import { rememberRoomBaseline } from './editors/room-form-state';
import { forgetMarkerBaseline, rememberMarkerBaseline } from './editors/marker-form-state';
import { commitPlanOptimization } from './plan-optimize-write';
import { openSpaceCopyDialog, renderSpaceCopyDialog, saveSpaceCopy } from './space-copy-runtime';
import { mdiHomeCityOutline } from '@mdi/js';
import { saveSpaceCopy } from './space-copy-runtime';
import {
Affine, applyAffine, readVacTelemetry,
autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, vacMapIdWithFallback,
parseVacSourceCandidate, resolveVacSource, resolveCurrentVacPath, trimVacPathTarget, areaCentroid,
vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM,
VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
FitParams, fitMatrix, initialFit, reanchorFit, VacRoom,
Pt as VacPt, type VacPath, type VacSourceCandidate,
type VacSourceResolution, type VacSourceStatus,
Affine, applyAffine, readVacTelemetry, autoCalibrate, vacTrailMode, areaCentroid,
vacCalibrationResidualCm, vacRoomNameMatchCount, VAC_CALIBRATION_WARN_CM, FitParams, fitMatrix,
reanchorFit, VacRoom, Pt as VacPt, type VacSourceCandidate, type VacSourceResolution,
} from './vacuum';
import {
buildDevices, deviceFromMarkerDraft, effectiveExcludedIntegrations, seedHiddenBindings, lqiFor, tempFor, humFor, climateTempFor, isHumEntity,
areaTemp, areaHum, sourceValue, areaClimateMap,
resolvedLightSources, resolvedLightState, resolvedLightStats,
hasOwnSpatialSource, hasOwnStatefulLightSource, ownControllableEntities,
forcedLightEntityOf,
resolveDeviceLightSettings, selectSpatialGlowSource,
resolvedDeviceStateEntities, removedPlanBindings, isRemovedPlanEntity,
deletePlanMarkerRecords, effectiveMarkerControls, persistedExternalControls,
removeMarkerControlReferences, rewriteMarkerControlReferences, markerControlWouldCycle,
resolveIcon,
type AreaClimate,
buildDevices, deviceFromMarkerDraft, effectiveExcludedIntegrations, seedHiddenBindings,
resolvedLightSources, hasOwnSpatialSource, ownControllableEntities, selectSpatialGlowSource,
removedPlanBindings, isRemovedPlanEntity, deletePlanMarkerRecords, effectiveMarkerControls,
persistedExternalControls, removeMarkerControlReferences, rewriteMarkerControlReferences,
markerControlWouldCycle,
} from './devices';
import {
bindingCandidates, buildDeviceInbox, filterDeviceInbox,
type DeviceInboxCategory, type DeviceInboxReason, type DeviceInboxRow,
} from './device-inbox';
import {
formatToggleConfirmation, formatToggleIntent, projectedTapAction, resolveToggleIntent,
sameToggleOperationTargets, toggleCoverEntity, toggleIntentName, toggleOperation,
toggleEntityCandidates, unavailableToggleTargetNames,
type ResolvedToggleIntent, type ResolvedToggleTarget,
type ToggleNextEffect, type ToggleNoneReason,
type ToggleSkipReason,
formatToggleIntent, projectedTapAction, type ResolvedToggleIntent, type ResolvedToggleTarget,
type ToggleNextEffect, type ToggleNoneReason, type ToggleSkipReason,
} from './device-toggle';
import { toggleEntityWriteFields } from './marker-toggle-entity';
import { removeMarkerAreaSnapshots } from './device-area-relocation';
import {
adoptVirtualLightServerSnapshot,
applyVirtualLightEvent,
reconcileVirtualLightSnapshot,
virtualLightFingerprint,
virtualLightSnapshot,
virtualLightWire,
type VirtualLightSnapshot,
} from './virtual-light-state';
import { type VirtualLightSnapshot } from './virtual-light-state';
import type {
OpeningCfg, PartitionOpeningHost,
RoomCfg, PartitionCfg, WallColumnCfg,
SpaceModel, PdfRef, Marker, ServerConfig, DevItem, CardConfig,
MarkerValueBadge, ValueBadgePosition, ValueBadgeSource, ZeroWallStyle,
OpeningCfg, PartitionOpeningHost, RoomCfg, PartitionCfg, WallColumnCfg, SpaceModel, PdfRef,
Marker, ServerConfig, DevItem, CardConfig, MarkerValueBadge, ValueBadgePosition,
ValueBadgeSource,
} from './types';
import {
radarAfterBindingChange, radarConfigFromDraft, radarDraft, recognizeRadar,
type RadarEditorDraft,
radarConfigFromDraft, radarDraft, recognizeRadar, type RadarEditorDraft,
} from './radar-editor';
import { finishMarkerDialogClose } from './marker-dialog-close';
import { renderRadarSection } from './editors/radar-section';
import { radarDiscardRequest, RadarSetupController } from './radar-setup';
import {
COLUMN_MAX_CM, canonicalColumnAngle, clampColumnCm, columnBody,
directionalOccluders, floorMinusBodies, geometryArea, geometryOuterRings,
geometryAllRings, intersectionPaths, partitionBody, polyclipPathD,
pointInOpaquePlanBody, pointInPhysicalBody, sameColumnPlacement,
physicalBodyParts,
type PartitionOpeningCut,
COLUMN_MAX_CM, canonicalColumnAngle, clampColumnCm, columnBody, floorMinusBodies, geometryArea,
partitionBody, pointInPhysicalBody, sameColumnPlacement, type PartitionOpeningCut,
} from './physical-geometry';
import {
hostedOpeningIntervalsOverlap, materializePartitionOpening,
partitionOpeningJambMargin, partitionOpeningNeedsStrictValidation,
partitionOpeningFace,
partitionPlacementIntervals, resolvePartitionOpeningCompat, resolvePartitionOpeningStrict,
type PartitionOpeningOrphanReason, type ResolvedPartitionOpening,
hostedOpeningIntervalsOverlap, materializePartitionOpening, partitionOpeningJambMargin,
partitionOpeningNeedsStrictValidation, partitionOpeningFace, partitionPlacementIntervals,
resolvePartitionOpeningCompat, resolvePartitionOpeningStrict,
} from './partition-openings';
import {
buildHiddenWallDiagnosticGeometry, buildPlanSnapGeometry,
@@ -226,20 +164,14 @@ import {
type PlanSnapCandidate, type PlanSnapEndpoint, type PlanSnapGeometry, type PlanSnapSegment,
} from './plan-snap-overlay';
import {
atomizeWallSegments, buildWallFaceGraph, findNewWallFacesInGraphs, findWallFaceAtPoint,
normalizeUnifiedWallTool, wallChainSegments, chainSegmentCms,
buildWallFaceGraph, findNewWallFacesInGraphs, findWallFaceAtPoint, chainSegmentCms,
type WallFaceGraph, type WallGraphFace, type WallGraphSourceSegment,
} from './wall-face-graph';
import { parameterOnPartition, planRoomDeletion } from './room-deletion';
import {
planWallFaceRepair, repairMovesHostedPartition, type WallFaceRepairProposal,
} from './wall-face-repair';
import {
LightSegment, polygonSegments, splitAtIntersections, visibilityPolygon,
} from './light-visibility';
import './space-card';
import { cardStyles } from './styles';
import { editorSecondaryStyles } from './editor-secondary.styles';
import {
EditorSecondaryController,
type EditorSecondaryCopy,
@@ -247,11 +179,8 @@ import {
type EditorToolbarGroup,
} from './editor-secondary';
import {
fitInSquare, planRect, contentBounds, spaceModels, contentFrame, contentItems, spaceFrame,
spaceCenter, iconUnit, iconCqw, gridLevels, itemOf, snapPt,
MIN_ZOOM, PAN_SLACK, CANVAS_LIMIT, SANE_LIMIT, GRID_PITCH, GRID_STEP_N,
PLAN_SCALE_MIN, PLAN_SCALE_MAX,
clampCanvasR, clampCanvasN, type ContentItem, type Rect,
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 {
@@ -260,29 +189,17 @@ import {
fixedTopologyWallLineageHints,
resolveRoomOpeningHost, wallModelOffGridValueCount, WallSegmentModelError,
} from './wall-segment-model';
import {
legacyZeroContourLines, resolveZeroWalls, zeroContourLines,
zeroWallHasOpening, zeroWallStyleOf,
} from './zero-walls';
import { resolveZeroWalls, zeroWallHasOpening, zeroWallStyleOf } from './zero-walls';
import { snapNearAxisEndpoint } from './near-axis';
import type { SpaceReferenceRepairContext } from './space-reference-repair';
import { collectSpaceMarkerDependencies, spaceDeletionMessage } from './space-deletion';
import {
checkSpacePhysicalGeometry,
checkOptimizeGeometry,
geometryOpenCuts,
geometryOpenings,
geometryPartitionOpeningCuts,
geometryRoomOpeningInputs,
spacePhysicalGeometryFingerprint,
type GeometryOpeningProjection,
checkSpacePhysicalGeometry, checkOptimizeGeometry, geometryOpenCuts, geometryOpenings,
geometryPartitionOpeningCuts, spacePhysicalGeometryFingerprint,
type OptimizeGeometryPreflightResult,
} from './plan-geometry-preflight';
import {
canonicalizeConfigGeometry,
canonicalizeLayoutGeometry,
canonicalizePosition,
formatLatticeShiftCm,
canonicalizeConfigGeometry, canonicalizePosition, formatLatticeShiftCm,
} from './coordinate-canonicalization';
import { enqueueSerializedWrite, type OptimisticAttempt } from './serialized-write-queue';
import { applyCalibrationProposal, saveAutomaticCalibration, saveManualCalibration, saveVacuumMatrix,
@@ -304,68 +221,29 @@ import {
import { classifyPlanFile, encodePlanFile, renderBackdropGuard } from './backdrop-pick';
import { CommandStack } from './command-stack';
import type { DeviceLayout, DevicePositionState } from './device-position-history';
import { resolvedSvgScreenBlend, svgScreenBlendSupported } from './glow-blend';
import {
CONTINUITY_LONG_HIDDEN_MS,
VisualContinuityController,
contentFingerprint,
subscribePageVisibility,
visualFrameFingerprint,
type PageVisibilitySignal,
} from './visual-continuity';
import { contentFingerprint } from './visual-continuity';
import { PointerModalityController } from './pointer-modality';
import { type ResolvedDevicePresentation } from './device-presentation';
import { type FiniteActivityRuntime } from './activity-runtime';
import {
entityVisualSample, entityVisualSamplesForDevice,
type DeviceActivity, type DeviceVisualState, type EntityVisualSample,
} from './device-visual';
import {
activitySourceSignature, deviceA11yState, presentationClasses, resolveDevicePresentation,
resolvePresentationSources, type ResolvedDevicePresentation,
} from './device-presentation';
import {
ACTIVITY_WINDOW_MS, advanceFiniteActivity, createFiniteActivityRuntime,
resetFiniteActivityRuntime, stampFiniteActivity, type FiniteActivityRuntime,
} from './activity-runtime';
import {
recommendedValueBadgeSource, valueBadgeCandidates, valueBadgeSourceFromKey,
valueBadgeSourceKey, valueBadgeTitle, valueBadgeWriteFields, valueSourceWriteFields,
recommendedValueBadgeSource, valueBadgeCandidates, valueBadgeWriteFields, valueSourceWriteFields,
type ValueBadgeCandidate,
} from './device-value-badge';
import {
createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions,
type RenderDeviceSnapshot,
} from './render-device-snapshot';
import { deviceFaceStyle, deviceThemeClass, renderDeviceFace } from './device-face';
import { effectiveDeviceBaseSize } from './device-marker-geometry';
import {
ModeTransitionController, viewportFromViewBox,
type HouseplanMode, type ModeTransitionState, type ModeVisualState, type ModeViewBox,
} from './mode-transition';
import {
currentLabs, hashSpace, noteLabsRender, subscribeLabs, type LabsSnapshot,
} from './labs';
import { type RenderDeviceSnapshot } from './render-device-snapshot';
import { type HouseplanMode, type ModeVisualState } from './mode-transition';
import { projectPlanPoint } from './iso-projection';
import {
acquireHaRegistries, activeRegistryHass, cacheHaBindingStatuses,
fullRegistryHass, haRegistryBuildSignature, haRegistryDiagnostics, haRegistrySnapshot,
openingEntityAvailable, refreshHaRegistries, renderOpeningEntityAvailable,
resolveHaBindingStatus,
type HaBindingStatus, type HaRegistrySnapshot,
} from './ha-binding-status';
import { type HaBindingStatus, type HaRegistrySnapshot } from './ha-binding-status';
import type { DecorShape, DecorStyle } from './editors/decor/types';
import {
DECOR_ASSETS_API_VERSION, type DecorAsset,
} from './decor-assets';
import { DecorImageEditor } from './decor-image-editor';
import {
DEFAULT_DECOR_STYLE, boxAnchors, boxCorners, clamp01, decorCmToUnits,
decorStrokeUnits, decorStyleOf, decorStylePatch,
decorUnitsToCm, mergeSnapGeometry, normalizeAngle, resizeDecorBox,
resizedBoxTopLeft, snapDecorPoint, validDecorDraft,
boxAnchors, clamp01, decorCmToUnits, decorStylePatch, decorUnitsToCm, mergeSnapGeometry,
normalizeAngle, resizeDecorBox, resizedBoxTopLeft, snapDecorPoint, validDecorDraft,
type DecorBox, type SnapGeometry,
} from './editors/decor/geometry';
import { decorStyleToSettings } from './editors/decor/geometry';
import { renderOpeningTunnelFills } from './render/opening-tunnels';
import {
openingVisibleMetrics, renderOpeningVisibleGeometry,
type OpeningFaceOffset, type OpeningVisibleSpec,
@@ -381,14 +259,7 @@ import {
} from './opening-dimensions';
import type { OpeningPlacementCandidate, OpMeasure, RenderOpening } from './interaction-types';
import { safeStoredColor } from './color';
import {
gridCellFieldToCm, gridCellFieldValue, gridVisualScale, gridVisualUnits,
newSpaceCellCm, wallThickHoverHalfUnits,
} from './grid-scale';
import {
applySpaceOrder, canStartTabDrag, markersNeedingPlacement, passedDragThreshold,
reorderSpaceIds,
} from './space-order';
import { gridCellFieldValue, gridVisualUnits, newSpaceCellCm } from './grid-scale';
import { applyOpeningMoves, mergeCollinearPartitions, spaceMergeGeometry } from './wall-merge';
import { reconcileCoincidentPartitions } from './coincident-partitions';
import { finalizeWallChainSpace } from './writer-fixed-point';
@@ -423,27 +294,6 @@ type ResizeWallUnion = ReturnType<typeof wallBodiesUnionPath>;
type ResizeWallArtifact = ReturnType<typeof wallBodiesGeometry>;
/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */
const DECOR_TEXT_CM_MAX = 2000;
/** HP-1552 boot-veil timing (AUD-1552-02). The veil holds for at least
* BOOT_MIN_MS; every stage-height change restarts a BOOT_QUIET_MS
* trailing-quiescence requirement (chrome still settling near the cap
* extends the wait); BOOT_MAX_MS lifts the veil unconditionally.
* BOOT_MIN_MS exceeds the old 600 ms window by a frame-latency margin: a
* panel applied right at the window's edge (~590 ms) only materializes in
* the stage height a couple of rAF/render frames later. */
const BOOT_MIN_MS = 700;
const BOOT_QUIET_MS = 250;
const BOOT_MAX_MS = 1200;
/** AUD-1552-02: post-reveal grace during which late chrome shifts glide
* (CSS height transition on the stage) instead of snapping. */
const BOOT_SOFT_MS = 1500;
type LruRead<V> = { hit: true; value: V } | { hit: false };
const lruRead = <K, V>(cache: Map<K, V>, key: K): LruRead<V> => {
if (!cache.has(key)) return { hit: false };
const value = cache.get(key)!;
cache.delete(key);
cache.set(key, value);
return { hit: true, value };
};
const lruWrite = <K, V>(cache: Map<K, V>, key: K, value: V, limit: number): void => {
cache.delete(key);
cache.set(key, value);
@@ -467,193 +317,14 @@ const lruWrite = <K, V>(cache: Map<K, V>, key: K, value: V, limit: number): void
* the key keeps two DIFFERENT cards on one page from adopting each other's
* header height (same config twice on one view is indistinguishable — and
* then the heights match anyway). */
/** DEV-B703-03 — what the dead instance was LOOKING at. The header height
* alone was not enough: `_view` (the pan) never left the instance, and the
* EDITOR zoom is deliberately not persisted (`_saveZoom` is view-only) while
* the editor MODE is (LS_NAV). So a re-mount inside an editor came back at
* the view-mode zoom, and a panned view came back re-centred on the plan —
* the owner's «чуть-чуть дёргается масштаб». The memo now carries the whole
* viewport, so the restore is the same rect, not the same zoom number. */
type WarmViewport = {
space: string;
mode: 'view' | 'plan' | 'devices' | 'decor';
projection: 'flat' | 'iso';
activeLabsIso: boolean;
logicalCenter: { x: number; y: number } | null;
zoom: number;
view: { x: number; y: number; w: number; h: number } | null;
/** the view-mode viewport an editor was entered from (_viewModeSnap) */
snap: { space: string; zoom: number; cx?: number; cy?: number } | null;
tool: MarkupTool;
decorTool: DecorTool;
showHidden: boolean;
/** «показать дальние» changes _baseVb, so the restored view rect is only
* the same rect if the frame it was clamped against is the same one */
showFar: boolean;
selId: string | null;
rszSel: string | null;
decorSel: string | null;
};
const expiredWarmViewport = (vp: WarmViewport | null): WarmViewport | null => {
if (!vp || vp.mode === 'view') return vp;
return {
...vp,
mode: 'view',
zoom: vp.snap?.space === vp.space ? vp.snap.zoom : vp.zoom,
view: null,
snap: null,
tool: 'draw',
decorTool: 'select',
showHidden: false,
selId: null,
rszSel: null,
decorSel: null,
};
};
/** Which dialog was open and its draft (docs/WARM-REMOUNT.md §3). `data` is
* the live draft OBJECT — the memo is module state, never serialised, so a
* half-filled device dialog with its uploaded pdfs survives for free. */
type WarmDialogKind = 'space' | 'marker' | 'settings' | 'opening' | 'decorText' | 'decorShape' | 'backdrop' | 'rules' | 'room' | 'info' | 'openingInfo';
type WarmDialog = { kind: WarmDialogKind; space: string; mode: string; data: unknown; baseline?: string };
/** AUD-159B1-01: one entry per CARD PLACEMENT, not per key. Two cards with an
* identical config on one view share the key, so the key alone cannot say
* whose viewport this is; `place`/`idx` (the parent element the card was
* mounted in, and its position among that parent's children) identify the
* DOM slot, and `owner` the live instance sitting in it. A re-mount into the
* same slot inherits the entry; a different card never does. */
type WarmEntry = {
/** generation id of the instance that currently owns the slot */
owner: number;
/** HA route where this placement was captured. */
path: string;
/** the parent element the owner was mounted in (weak — never keep DOM alive) */
place: WeakRef<Node> | null;
/** the owner's index among that parent's children */
idx: number;
/** the owner is attached; a dead slot is a tombstone waiting for a successor */
live: boolean;
hdrH: number;
stageH: number;
vp: WarmViewport | null;
/** Last complete visual frame for #73; safe only inside the same placement. */
frameFingerprint: string;
/** Metadata projection is immutable-by-replacement and avoids an empty
* device layer on a same-document warm remount before registry refresh. */
devices: readonly DevItem[] | null;
dlg: WarmDialog | null;
/** when the instance that wrote `dlg` detached; 0 = it is still alive */
freed: number;
/** the TTL timer that frees `dlg` once it can no longer be revived */
evict: number;
};
const warmBoot = new Map<string, WarmEntry[]>();
let warmGen = 0;
/** `location.pathname` is the dashboard AND the view path: two Lovelace views
* never share a key, so a card that comes back on another view boots cold
* instead of inheriting a stranger's viewport (AUD-159B1-01). The hash is
* deliberately out — `#space=` is OUR deep link, not another placement. */
const warmBootKey = (config: unknown): string =>
`${window.innerWidth}x${window.innerHeight}|${location.pathname}|${JSON.stringify(config ?? {})}`;
/** A dialog is revived only if the instance that owned it died THIS long ago.
* A Lovelace rebuild detaches and re-attaches within one task; a user who
* walked off to another dashboard view and came back later must not be met
* by a dialog they have long forgotten opening. */
let WARM_REVIVE_MS = 10000;
/** The memo gains a key on every window RESIZE and never loses one; the
* values used to be two numbers, and now they can hold a dialog draft (a
* plan pdf among it). Keep the last few viewports — a stale entry only
* costs the next card at that size a cold boot. */
const WARM_MAX_KEYS = 8;
/** How many placements of ONE key are remembered. More identical cards than
* this on one view and the oldest dead slot is dropped — it only costs that
* placement a cold boot. */
const WARM_MAX_SLOTS = 4;
/** Which slot of `list` belongs to the instance now claiming it, and may we
* trust it with the viewport/dialog (`sure`) or only with the settled height?
* Ranked, best first (AUD-159B1-01):
* 4 — same parent, same index: literally the DOM slot we are standing in,
* which is how Lovelace replaces a card (the predecessor may still be
* attached for another task — that is the case the audit reproduced);
* 0 — some OTHER placement whose owner is still attached: a neighbouring
* card with an identical config, never ours to inherit;
* 3 — same parent, shifted index, owner gone: still our placement;
* 2 — a tombstone whose placement is gone with its subtree — the ordinary
* Lovelace rebuild, where the container is rebuilt too.
* A tie means two candidates are equally plausible: then only the settled
* height is adopted, and it is the same for all of them anyway. */
const warmMatch = (
list: WarmEntry[],
gen: number,
place: Node | null,
idx: number,
): { slot: WarmEntry | null; sure: boolean } => {
const score = (s: WarmEntry): number => {
const same = !!place && s.place?.deref() === place;
if (same && s.idx === idx) return 4;
if (s.live) return 0;
return same ? 3 : 2;
};
let best: WarmEntry | null = null;
let bestScore = 0;
let ties = 0;
let newest: WarmEntry | null = null;
for (const s of list) {
if (s.owner === gen) continue;
newest = s;
const sc = score(s);
if (sc <= 0) continue;
if (sc > bestScore) { best = s; bestScore = sc; ties = 1; }
else if (sc === bestScore) ties++;
}
if (!best || ties > 1) return { slot: best || newest, sure: false };
return { slot: best, sure: true };
};
/** Rotation step of a decor text block — the same 5° a device icon turns in
* (marker dialog). Shift affects angle precision only, never position. */
const DT_ANGLE_STEP = 5;
/** Line spacing of a multi-line label, in font sizes. */
const DT_LINE = 1.2;
const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
const LS_NAV = 'houseplan_card_nav_v1'; // last space only; editor sessions never survive page navigation
const LS_KIOSK = 'houseplan_card_kiosk_v1'; // per-SCREEN size multipliers (each wall tablet differs)
const LS_VIEW = 'houseplan_card_view_v1'; // presentation preference per space, Labs-only
const POINTER_HOVER_TARGET_SELECTOR = 'hp-dialog, hp-help, hp-color-opacity, hp-device-preview';
const NORM_W = 1000; // side of the render space — the canvas is square (v1.48.0)
/** Short semantic-event / direct-terminal-transition window. Event uses
three sequential 1.1 s waves; motion cool-down itself never animates. */
/**
* How finely the lit region is traced where nothing blocks the light. 96 steps
* put the chord error at 0.05% of the radius — under a tenth of a pixel on a
* wall tablet, and cheap because only unobstructed directions use them.
*/
const GLOW_ARC_STEPS = 96;
/**
* Width of the lit→unlit ramp along a shadow edge, in SCREEN pixels: the eye
* reads a perfectly geometric edge as a cut-out, and a real penumbra is never
* wider than a hair at this scale. Measured on screen on purpose, so zooming in
* does not turn a hairline into a smear.
*/
const GLOW_EDGE_FEATHER_PX = 2;
/**
* Radial profile of a pool, as [offset %, share of the calibrated alpha].
* Monotonic all the way out: a lamp is brightest under itself and dies at its
* radius. The centre keeps the full calibrated alpha, so nothing about the
* brightness maths (docs/specs/067) changes — only where that alpha is spent.
*/
const GLOW_FALLOFF: readonly (readonly [number, number])[] = [
[0, 1], [45, 0.88], [70, 0.62], [86, 0.32], [100, 0],
];
/** A source pool fades in/out without changing its final calibrated alpha. */
const GLOW_FADE_MS = 500;
/** Smallest rectangle holding both (docs/CANVAS.md §4). */
const unionRect = (a: Rect, b: Rect): Rect => {
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
return { x, y, w: Math.max(a.x + a.w, b.x + b.w) - x, h: Math.max(a.y + a.h, b.y + b.h) - y };
};
/** #313: one Thickness-tool hit — a room interval or independent masonry. */
type WallThickSource = { kind: 'room' }
@@ -664,10 +335,6 @@ type WallThickHit = {
};
type MarkupTool = 'select' | 'draw' | 'column' | 'merge' | 'split' | 'resize' | 'opening' | 'wallthick' | 'delroom';
type RoomFillFrame = {
byRoom: Map<RoomCfg, ResolvedRoomFill | null>;
byId: Map<string, ResolvedRoomFill | null>;
};
type WallFaceCandidate = WallGraphFace & {
split?: { roomId: string; mainPoly: number[][]; newPoly: number[][] };
consumeAllActive?: boolean;
@@ -690,28 +357,6 @@ type WallFaceBatch = {
activeCms: number[];
activePartitionIds: string[];
};
/**
* The floor a source can see, and nothing else. One region means one clip:
* a beam through a doorway, the room it lands in and the shadow of a column
* are all the same computation, so they can never disagree with each other.
*/
type GlowClipGeometry = { lit: string[] };
const MARKUP_TOOLS = new Set<MarkupTool>([
'select', 'draw', 'column', 'merge', 'split', 'resize',
'opening', 'wallthick', 'delroom',
]);
/** Warm viewport is page-memory, so it may contain a tool name from the old bundle. */
const normalizeMarkupTool = (value: unknown): MarkupTool => {
// #173 replaces the public one-shot Partition tool with one Walls chain.
// A warm page may still carry the old session token; reading it is inert.
value = normalizeUnifiedWallTool(value);
// Opening placement is valid only together with its explicit session-only
// type preset. Warm viewport state does not persist that preset.
if (value === 'opening') return 'draw';
return typeof value === 'string' && MARKUP_TOOLS.has(value as MarkupTool)
? value as MarkupTool
: 'draw';
};
const MAX_WALL_CHAIN_POINTS = 500;
const MAX_PARTITIONS = 2000;
const MAX_ROOMS = 400;
@@ -740,17 +385,6 @@ interface SpaceGeometryState {
* (docs/FURNITURE.md): it opens a palette and places a symbol at real size. */
type DecorTool = 'select' | 'backdrop' | 'line' | 'rect' | 'ellipse' | 'text' | 'furniture' | 'image' | 'erase';
const fireEvent = (node: EventTarget, type: string, detail?: unknown) => {
const ev = new Event(type, { bubbles: true, composed: true }) as any;
ev.detail = detail ?? {};
node.dispatchEvent(ev);
};
const navigate = (path: string) => {
history.pushState(null, '', path);
fireEvent(window, 'location-changed', { replace: false });
};
/**
* Debounce with `flush()` and `pending`. Both are load-bearing: a pending
* config write MUST be flushed before the card adopts a server revision,
@@ -763,36 +397,6 @@ interface Debounced<T extends (...a: any[]) => void> {
pending(): boolean;
}
const debounce = <T extends (...a: any[]) => void>(fn: T, ms: number): Debounced<T> => {
let t: number | undefined;
let last: Parameters<T> | null = null;
const wrapped = ((...a: Parameters<T>) => {
clearTimeout(t);
last = a;
t = window.setTimeout(() => {
t = undefined;
const args = last;
last = null;
if (args) fn(...args);
}, ms);
}) as Debounced<T>;
wrapped.flush = () => {
if (t === undefined) return;
clearTimeout(t);
t = undefined;
const args = last;
last = null;
if (args) fn(...args);
};
wrapped.cancel = () => {
clearTimeout(t);
t = undefined;
last = null;
};
wrapped.pending = () => t !== undefined;
return wrapped;
};
/**
* Capture the pointer for a drag, tolerating an inactive pointerId.
*
@@ -825,8 +429,6 @@ interface DeviceInboxDialogState {
busy?: string;
}
type FixedFloorState = FixedFloorSelection | { kind: 'pending'; value: unknown };
export interface HouseplanEditorHostPort {
_ackNewDevice: (id: string) => void;
_activeWallChainId: string | null;
@@ -6894,8 +6496,6 @@ public _applyWallFaceBatch(): void {
abort('toast.wall_repair_changed');
return;
}
const effectiveActivePath = this._activePathWithRepair(batch.activePath, repairs[0]);
if (!accepted.length) {
if (!this._finalizeWallChainPartitions(batch.activePartitionIds)) return;
this.host._path = [];
+1 -3
View File
@@ -9,9 +9,7 @@ import {
import { mapTopologyNodes, TOPOLOGY_STALE_MS } from './zigbee-topology';
import type { DevItem } from './types';
import type { HaRegistrySnapshot } from './ha-binding-status';
import type {
ZigbeeProviderState, ZigbeeTopologyHass, ZigbeeTopologyRuntimeSnapshot,
} from './zigbee-topology-runtime';
import type { ZigbeeTopologyHass, ZigbeeTopologyRuntimeSnapshot } from './zigbee-topology-runtime';
const EMPTY_RUNTIME: ZigbeeTopologyRuntimeSnapshot = { revision: 0, topologies: [], states: {} };
-1
View File
@@ -6,7 +6,6 @@
* SVG volume layers so the main Lit shell does not also become a renderer.
*/
import { nothing, svg, type TemplateResult } from 'lit';
import { guard } from 'lit/directives/guard.js';
import { clampScale, islandsOf, roomPoly, type SpaceDisplay } from './logic';
import {
ISO_CAMERA, ISO_FLOOR_EDGE_HEIGHT, ISO_OVERLAY_VISUAL_OFFSET,
-12
View File
@@ -100,18 +100,6 @@ const simplePolygon = (poly: number[][]): boolean => {
return polygonArea(poly) > 1e-12;
};
const simpleOpenPath = (points: number[][]): boolean => {
for (let i = 0; i + 1 < points.length; i++) {
if (samePoint(points[i], points[i + 1])) return false;
for (let j = i + 2; j + 1 < points.length; j++) {
if (segmentsProperlyCross(points[i], points[i + 1], points[j], points[j + 1])) {
return false;
}
}
}
return true;
};
const replacePoint = (point: number[], move: EndpointMove): number[] => (
samePoint(point, move.from) ? [...move.to] : [...point]
);
+1 -1
View File
@@ -1,5 +1,5 @@
import { html, type TemplateResult } from 'lit';
import type { PdfDialogContext, PdfAssetReference } from './hp-pdf-dialog';
import type { PdfDialogContext } from './hp-pdf-dialog';
import './hp-pdf-dialog';
import { buildPdfPage, type PdfExportOptions, type PdfRasterPlacement } from './pdf-scene';
import { writePdf } from './pdf-writer';
+1 -1
View File
@@ -5,7 +5,7 @@ import {
linearWallBody, linearWallJoinPatches, pairButtEndTrimWedges, wallCmToUnits,
type LinearWallSegment,
} from './wall-thickness';
import type { PartitionCfg, SpaceModel, WallColumnCfg } from './types';
import type { SpaceModel, WallColumnCfg } from './types';
export const COLUMN_MIN_CM = 1;
export const COLUMN_MAX_CM = 150;
-1
View File
@@ -1192,7 +1192,6 @@ export function validateSafeResize(
}
}
const movingRoom = rooms.find((room) => room.id === plan.roomId)!;
const movingPoly = result.polys[plan.roomId];
const movingEdge = plan.edgeByRoom[plan.roomId];
const ma = movingPoly[movingEdge];
+1 -1
View File
@@ -22,7 +22,7 @@ import {
wallHatchStepUnits, wallIntervals, innerContourForRoom, HATCH_BASE_STEP_UNITS,
type WallEntry,
} from './wall-thickness';
import { DEFAULT_ICON_RULES, compileIconRules, EXCLUDED_DOMAINS } from './rules';
import { DEFAULT_ICON_RULES, compileIconRules } from './rules';
import { t, type Lang } from './i18n';
import { bgModeOf, resolveDayCycle } from './sun';
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
-5
View File
@@ -820,11 +820,6 @@ export class LoadedSummaryPanelRuntime {
return resolved === null ? { kind: 'unavailable' } : { kind: 'ready', text: resolved };
}
private value(value: SummaryPanelValue): string {
const state = this.valueState(value);
return state.kind === 'ready' ? state.text : this.t('summary.unavailable');
}
private ensureMetrics(): void {
if (this.metricsModule || this.metricsLoad) return;
const generation = this.lifecycleGeneration;
-1
View File
@@ -11,7 +11,6 @@
* each guessing a floor. Nothing here touches the DOM, `hass` or storage.
*/
import { FitParams, fitFromMatrix, initialFit, VacRoom } from './vacuum';
export type Affine = [number, number, number, number, number, number];
-3
View File
@@ -4323,9 +4323,6 @@ export function paperRoomShapesWithWalls(
const rawPaper = exterior.shell?.length
? union(exterior.centre, exterior.shell)
: exterior.centre;
const multiWallNodes = multiWallNodesForGeometry(
rooms, walls, openCuts, pitch, cellCm, gridPitch, coordScale,
);
const paper = rawPaper;
const path = polyclipToPathD(paper);
if (path) return [{ path }];
+9 -2
View File
@@ -29,7 +29,11 @@ const CAPS = {
// 2026-09-14, #564: the geometry/index/latch/DOM ownership implementation is
// isolated in device-hit-owner.ts. These 81 lines are the remaining card
// lifecycle invalidations and the presentation/action integration seams.
'src/houseplan-card.ts': 13732,
// 2026-09-23, #624: сняты 112 мёртвых делегатов и полей, которых не читает
// ни карточка, ни харнесс (реализация живёт в рантайме и зовётся оттуда),
// 190 неиспользуемых импортов и 9 мёртвых констант — 784 строки. Потолок
// опущен на выигрыш, запас над фактом три строки.
'src/houseplan-card.ts': 12951,
// #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.
@@ -38,7 +42,10 @@ const CAPS = {
// — 1290 строк. Потолок опущен ровно на эту величину (14100 → 12810), запас
// над фактом остался прежним, три строки: выигрыш зафиксирован, иначе ядро
// отыграет его обратно первой же правкой диалога.
'src/houseplan-editor-runtime.ts': 12810,
// 2026-09-23, #624: снесены мёртвые копии объявлений карточки (warm-boot,
// LS_*, GLOW_*, debounce, navigate, lruRead — 31 объявление и 6 типов) и
// 196 неиспользуемых импортов — 400 строк. Потолок опущен на выигрыш.
'src/houseplan-editor-runtime.ts': 12359,
};
/**
+19
View File
@@ -80,6 +80,7 @@ export function readHouseplanProductionSource() {
.replace(/^public\b/, visibility)
.replaceAll('this.host.', 'this.');
replacements.push({
name,
from: member.getStart(cardFile),
to: member.end,
text: implementationText,
@@ -91,6 +92,24 @@ export function readHouseplanProductionSource() {
+ replacement.text
+ reconstructed.slice(replacement.to);
}
// #624: мёртвые делегаты карточки удалены — реализация, у которой в
// карточке больше нет заглушки, живёт только в рантайме. Логическая
// поверхность продукта от этого не меняется, поэтому такие члены
// дописываются к исходнику в том же виде, в каком раньше подставлялись на
// место делегата. Контракт «что делает продукт» не должен зависеть от того,
// есть ли у метода однострочная заглушка в карточке.
const delegated = new Set(replacements.map((replacement) => replacement.name));
const appended = [];
for (const [name, implementation] of implementations) {
if (delegated.has(name) || name === 'host') continue;
const text = moved.has(name)
? `public ${name}(): TemplateResult ${moved.get(name)}`
: runtime.slice(implementation.getStart(runtimeFile), implementation.end);
appended.push(text.replace(/^public\b/, 'private').replaceAll('this.host.', 'this.'));
}
if (appended.length) {
reconstructed += `\n// --- editor runtime members without a card delegate (#624) ---\n${appended.join('\n\n')}\n`;
}
for (const path of SHARED_DIALOG_MODULES) {
const shared = readFileSync(new URL(path, import.meta.url), 'utf8')
.replaceAll('port.host.', 'this.')
+6 -2
View File
@@ -319,9 +319,13 @@ test('issue 252 Optimize keeps internal ids out of the main orphan report', () =
});
test('i18n: every literal help call has body and full aria keys in every language', () => {
const allCalls = helpSource.match(/this\._help\(/g) || [];
// #624: логический исходник теперь включает члены рантайма без делегата в
// карточке; среди них — порт общего модуля `help: (key) => this._help(key)`.
// Это проброс, а не вызов с ключом: литерал стоит у вызывающей стороны.
const allCalls = (helpSource.match(/this\._help\(/g) || []).length
- (helpSource.match(/this\._help\(key\)/g) || []).length;
const helpKeys = [...helpSource.matchAll(/this\._help\('([^']+\.help)'\)/g)].map((match) => match[1]);
assert.equal(helpKeys.length, allCalls.length, 'every _help call must use one string literal ending in .help');
assert.equal(helpKeys.length, allCalls, 'every _help call must use one string literal ending in .help');
assert.ok(helpKeys.length > 0, 'the help affordance pilot disappeared');
for (const key of helpKeys) {
for (const { code } of LANGUAGE_REGISTRY) {
+163
View File
@@ -0,0 +1,163 @@
// #624: шесть чисел связности монолита и гейт мёртвого кода — один модуль
// для `lint:unused` и `inventory`. Имена файлов монолита — через константы
// модуля: этот тест не читает монолит и не должен попадать в заморозку якорей. Фикстуры с известными числами: делегаты во
// всех формах, порт, `host.`, разбор диагностик, байты dist, храповик.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
BASELINE_FILE, BUNDLE_BYTES_BAND, CARD_FILE, METRIC_NAMES, RUNTIME_FILE, bundleBytes, classifyUnused, collectMetrics, compareWithBaseline, countDelegates,
harnessMemberNames, hostReferences, portMemberNames, readBaseline,
} from '../scripts/monolith-metrics.mjs';
import { baselineFrom, decide } from '../scripts/unused-locals-gate.mjs';
const CARD = `
import { html } from 'lit';
class HouseplanCard {
private _a = 1;
// делегаты: return, async, без return, геттер, await
private _saveMarker(): void { return this._editorRuntimeOrThrow()._saveMarker(); }
private async _saveRoom(id: string): Promise<void> { return this._editorRuntimeOrThrow()._saveRoom(id); }
private _open(): void { this._editorRuntimeOrThrow()._open(); }
private get _tool(): string { return this._editorRuntimeOrThrow()._tool; }
private async _apply(): Promise<void> { await this._editorRuntimeOrThrow()._apply(); }
// не делегаты: два выражения, свой код, сам аксессор
private _two(): void { this._a += 1; this._editorRuntimeOrThrow()._open(); }
private _own(): number { return this._a * 2; }
private _runtime() { return this._editorRuntimeOrThrow(); }
private _editorRuntimeOrThrow(): any { return null; }
}
`;
const RUNTIME = `
export interface HouseplanEditorHostPort {
_a: number;
_tool: string;
_saveRoom(id: string): Promise<void>;
readonly _model: unknown[];
'quoted-name'?: string;
}
class HouseplanEditorRuntime {
constructor(public host: HouseplanEditorHostPort) {}
run() { return this.host._a + this.host._tool.length; }
}
`;
test('#624 делегаты считаются по AST во всех формах, свой код — нет', () => {
assert.equal(countDelegates(CARD), 5);
assert.equal(countDelegates('class HouseplanCard { private _x() { return 1; } }'), 0);
assert.throws(() => countDelegates('class Other {}'), /HouseplanCard/);
});
test('#624 члены порта и обращения host. считаются по именам', () => {
const port = portMemberNames(RUNTIME);
assert.deepEqual([...port].sort(), ['_a', '_model', '_saveRoom', '_tool', 'quoted-name']);
const refs = hostReferences({
[RUNTIME_FILE]: RUNTIME,
'src/other.ts': 'const x = host._model; const y = port.host._a; ghost._z; hostile._q;',
[CARD_FILE]: 'host._ignored; host._ignored2;', // карточка не считается
});
assert.equal(refs.count, 4, 'два в рантайме + два в other; ghost./hostile. — не host.');
assert.deepEqual([...refs.names].sort(), ['_a', '_model', '_tool']);
});
test('#624 харнесс: имена, которых смоки касаются как свойств', () => {
const names = harnessMemberNames({
'demo/smoke_a.mjs': "card._saveMarker(); c._glowClipCache.clear(); Number(c._until || 0); el['_bracket'] = 1; card.publicName();",
});
assert.deepEqual([...names].sort(), ['_bracket', '_glowClipCache', '_saveMarker', '_until']);
});
test('#624 разбор диагностик: порт и харнесс разрешены и посчитаны, остальное — нарушение', () => {
const cardText = [
'class HouseplanCard {',
' private _portOnly = 1;',
' private _hostOnly(): void {}',
' private _harnessOnly = new Map();',
' private _dead = 2;',
' publicDead = 3;',
'}',
].join('\n');
const d = (line, name, file = CARD_FILE, code = 6133) => ({ file, line, code, message: `'${name}' is declared but its value is never read.` });
const { allowed, violations } = classifyUnused([
d(2, '_portOnly'), d(3, '_hostOnly'), d(4, '_harnessOnly'), d(5, '_dead'), d(6, 'publicDead'),
d(1, '_portOnly', 'src/other.ts'), // не карточка — не разрешается, даже если имя из порта
{ file: 'src/x.ts', line: 1, code: 6192, message: 'All imports in import declaration are unused.' },
], {
cardText, portNames: new Set(['_portOnly']), hostNames: new Set(['_hostOnly']), harnessNames: new Set(['_harnessOnly', '_dead_no']),
});
assert.deepEqual(allowed.port, ['_portOnly', '_hostOnly']);
assert.deepEqual(allowed.harness, ['_harnessOnly']);
assert.deepEqual(violations.map((v) => `${v.file}:${v.line}:${v.name}`), [
`${CARD_FILE}:5:_dead`, `${CARD_FILE}:6:publicDead`, 'src/other.ts:1:_portOnly', 'src/x.ts:1:',
]);
});
test('#624 байты dist — сумма всех файлов, включая чанки; без сборки — null', () => {
const dir = mkdtempSync(join(tmpdir(), 'hp-monolith-dist-'));
try {
mkdirSync(join(dir, 'dist', 'houseplan-assets'), { recursive: true });
writeFileSync(join(dir, 'dist', 'houseplan-card.js'), 'a'.repeat(10));
writeFileSync(join(dir, 'dist', 'houseplan-assets', 'chunk.js'), 'b'.repeat(1000));
assert.equal(bundleBytes(join(dir, 'dist')), 1010);
assert.equal(bundleBytes(join(dir, 'missing')), null);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('#624 храповик: рост на 1 любого числа — красный, снижение без записи базы — тоже, равенство — зелёный', () => {
const base = { delegates: 10, portMembers: 20, hostRefs: 30, portPrivates: 4, harnessPrivates: 5, bundleBytes: 1000 };
assert.deepEqual(compareWithBaseline({ ...base }, base), { grown: [], shrunk: [] });
for (const name of METRIC_NAMES.filter((n) => n !== 'bundleBytes')) {
const grown = compareWithBaseline({ ...base, [name]: base[name] + 1 }, base).grown;
assert.deepEqual(grown.map((g) => g.name), [name], `${name} +1 виден как рост`);
const decision = decide({ metrics: { ...base, [name]: base[name] + 1 }, violations: [], baseline: base });
assert.equal(decision.fail, true, `${name} +1 — красный гейт`);
assert.match(decision.lines.join('\n'), new RegExp(`связность выросла: ${name} `));
}
// bundleBytes — с полосой, как gzip-потолок #438: чужой коммит в dev меняет
// dist на сотни байт, и точное число красило бы каждую ветку после ребейза.
assert.deepEqual(compareWithBaseline({ ...base, bundleBytes: base.bundleBytes + BUNDLE_BYTES_BAND }, base).grown, []);
assert.deepEqual(compareWithBaseline({ ...base, bundleBytes: base.bundleBytes + BUNDLE_BYTES_BAND + 1 }, base).grown.map((g) => g.name), ['bundleBytes']);
assert.deepEqual(compareWithBaseline({ ...base, bundleBytes: base.bundleBytes - BUNDLE_BYTES_BAND - 1 }, base).shrunk.map((g) => g.name), ['bundleBytes']);
assert.equal(decide({ metrics: { ...base, bundleBytes: base.bundleBytes + BUNDLE_BYTES_BAND + 1 }, violations: [], baseline: base }).fail, true);
const shrunk = decide({ metrics: { ...base, delegates: 9 }, violations: [], baseline: base });
assert.equal(shrunk.fail, true);
assert.match(shrunk.lines.join('\n'), /база не опущена: delegates 10 → 9/);
assert.equal(decide({ metrics: base, violations: [], baseline: base }).fail, false);
// Отсутствие числа в базе — рост (нельзя обнулить метрику, удалив ключ).
const { portPrivates, ...withoutOne } = base;
void portPrivates;
assert.deepEqual(compareWithBaseline(base, withoutOne).grown.map((g) => g.name), ['portPrivates']);
});
test('#624 гейт: нарушения noUnusedLocals красят даже при равной базе; без dist — красный; без базы — красный', () => {
const base = { delegates: 1, portMembers: 1, hostRefs: 1, portPrivates: 1, harnessPrivates: 1, bundleBytes: 1 };
const violation = { file: 'src/x.ts', line: 3, code: 6133, message: "'y' is declared but its value is never read.", name: 'y' };
const red = decide({ metrics: base, violations: [violation], baseline: base });
assert.equal(red.fail, true);
assert.match(red.lines.join('\n'), /src\/x\.ts:3 TS6133/);
assert.equal(decide({ metrics: { ...base, bundleBytes: null }, violations: [], baseline: base }).fail, true);
assert.equal(decide({ metrics: base, violations: [], baseline: null }).fail, true);
assert.deepEqual(Object.keys(baselineFrom({ ...base, extra: 1 })), METRIC_NAMES);
});
test('#624 живое дерево: база равна текущим числам, нарушений нет (gate:small и Validate судят то же)', () => {
const root = fileURLToPath(new URL('../', import.meta.url));
const baseline = readBaseline(root);
assert.ok(baseline, `${BASELINE_FILE} обязан существовать`);
// Компилятор гоняет вся программа (~8 с): один раз здесь, как и в гейте.
const { metrics, violations } = collectMetrics(root);
assert.deepEqual(violations, [], 'мёртвый код по noUnusedLocals вне порта и харнесса');
const { grown, shrunk } = compareWithBaseline(metrics, baseline);
// bundleBytes судит гейт после сборки (Validate: Unit tests идут до Build,
// локально dist может быть от другого дерева) — здесь пять чисел исходника.
const source = (list) => list.filter((s) => s.name !== 'bundleBytes');
assert.deepEqual(source(grown), [], 'связность выросла — вернуть или обосновать');
assert.deepEqual(source(shrunk), [], 'связность упала — опустить базу: node scripts/unused-locals-gate.mjs --update');
assert.equal(typeof readFileSync(join(root, BASELINE_FILE), 'utf8'), 'string');
});
+70
View File
@@ -0,0 +1,70 @@
// #624: заморозка текстовых якорей монолита.
//
// 54 тест-файла читают `src/houseplan-card.ts` / `src/houseplan-editor-runtime.ts`
// как текст (напрямую или через `test/houseplan-source.mjs`). Каждый такой
// тест красится на переносе метода из карточки в модуль без единой регрессии
// — и это главная причина, по которой монолит не разбирается: перенос стоит
// дороже, чем оставить как есть. Новые утверждения о монолите доказываются
// экспортом и `test-build` (PROCESS.md §2.7), поэтому список ниже может
// только уменьшаться. Новое имя здесь — не «добавьте в список», а вопрос
// «почему контракт нельзя проверить исполнением».
import test from 'node:test';
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
/** Признак якоря: имя монолита или общий хелпер-реконструктор в тексте теста. */
export const MONOLITH_ANCHOR_RE = /houseplan-source\.mjs|houseplan-(?:card|editor-runtime)\.ts/;
/** Замороженный список — 2026-09-23, #624. Уменьшать свободно. */
export const FROZEN_TEXT_ANCHOR_TESTS = [
'bundle-assets.test.mjs', 'card-version.test.mjs', 'check-inputs.test.mjs', 'classify-changes.test.mjs',
'coincident-partitions.test.mjs', 'color-picker.test.mjs', 'config-adoption-ownership.test.mjs',
'coordinate-canonicalization.test.mjs', 'coordinate-write-barrier-guard.test.mjs', 'core-file-budget.test.mjs',
'danger-confirmation.test.mjs', 'data-hp-contract.test.mjs', 'device-hit-owner-contract.test.mjs',
'device-marker-polish-contract.test.mjs', 'device-position-echo.test.mjs', 'device-presentation-policy.test.mjs',
'device-presentation.test.mjs', 'devices.test.mjs', 'draft-live-preflight.test.mjs', 'editor-dialog-modules.test.mjs',
'editor-runtime-loader.test.mjs', 'fixed-floor-contract.test.mjs', 'furniture-assets.test.mjs',
'furniture-stroke-contract.test.mjs', 'furniture-transform-contract.test.mjs', 'golden-matrix.test.mjs',
'houseplan-panel.test.mjs', 'hp-dialog-contract.test.mjs', 'i18n.test.mjs', 'isometric-contract.test.mjs',
'junction-limits.test.mjs', 'mutation-gate.test.mjs', 'native-select-contract.test.mjs', 'near-axis.test.mjs',
'open-passage-contract.test.mjs', 'optional-space-model-contract.test.mjs', 'paper-scene-contract.test.mjs',
'performance-contract.test.mjs', 'process-gate.test.mjs', 'rebase-on-dev.test.mjs', 'release-contract.test.mjs',
'render-device-snapshot.test.mjs', 'resize-production-path.test.mjs', 'review-doc-guard.test.mjs',
'room-temperature-renderers.test.mjs', 'source-fingerprint.test.mjs', 'summary-panel.test.mjs',
'support-feedback.test.mjs', 'unified-wall-tool-source.test.mjs', 'version-recovery-card-contract.test.mjs',
'view-accessibility.test.mjs', 'wall-segment-model.test.mjs', 'wall-union-isolation.test.mjs',
'writer-fixed-point.test.mjs',
];
/** Файлы, которых нет в замороженном списке, но которые читают монолит текстом. */
export function newTextAnchors(actual, frozen = FROZEN_TEXT_ANCHOR_TESTS) {
const allowed = new Set(frozen);
return actual.filter((name) => !allowed.has(name)).sort();
}
export function textAnchorTests(dir) {
return readdirSync(dir)
.filter((name) => name.endsWith('.test.mjs') && name !== 'monolith-text-anchors.test.mjs')
.filter((name) => MONOLITH_ANCHOR_RE.test(readFileSync(join(dir, name), 'utf8')))
.sort();
}
test('#624 AC4: ни одного нового теста, читающего монолит как текст', () => {
const dir = fileURLToPath(new URL('./', import.meta.url));
const actual = textAnchorTests(dir);
assert.deepEqual(newTextAnchors(actual), [],
'новый контракт по монолиту доказывается экспортом и test-build, не regex по src/houseplan-card.ts (PROCESS.md §2.7, #624)');
// Список — верхняя граница, не инвентарь: ушедшие имена не возвращаются.
assert.ok(actual.length <= FROZEN_TEXT_ANCHOR_TESTS.length);
});
test('#624 AC4: подмена имени и новый файл видны как новые якоря, а не как «столько же»', () => {
const frozen = ['a.test.mjs', 'b.test.mjs'];
assert.deepEqual(newTextAnchors(['a.test.mjs', 'b.test.mjs'], frozen), []);
assert.deepEqual(newTextAnchors(['a.test.mjs'], frozen), [], 'уменьшение свободно');
// Та же длина, другое имя: сравнение по множеству, не по числу.
assert.deepEqual(newTextAnchors(['a.test.mjs', 'c.test.mjs'], frozen), ['c.test.mjs']);
assert.deepEqual(newTextAnchors(['a.test.mjs', 'b.test.mjs', 'c.test.mjs'], frozen), ['c.test.mjs']);
});
+2 -1
View File
@@ -28,7 +28,8 @@ test('#290 authoring, Optimize and renderer use one near-axis threshold source',
assert.match(renderer, /import \{ NEAR_AXIS_MAX_DEGREES \} from '\.\/near-axis';/);
assert.match(renderer,
/export const MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES = NEAR_AXIS_MAX_DEGREES;/);
assert.match(authoring, /import \{ snapNearAxisEndpoint \} from '\.\/near-axis';/);
// #624: импорт живёт в редакторском рантайме — карточка держала мёртвую копию.
assert.match(sourceOf('houseplan-editor-runtime.ts'), /import \{ snapNearAxisEndpoint \} from '\.\/near-axis';/);
assert.match(authoring, /snapNearAxisEndpoint\(/);
assert.match(optimizer, /import \{ repairNearAxisRoomWalls \} from '\.\/near-axis';/);
assert.match(optimizer, /repairNearAxisRoomWalls\(space\)/);
+3 -1
View File
@@ -97,8 +97,10 @@ const methodBody = (source, name) => {
test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass', () => {
const source = readHouseplanProductionSource();
// #624: `_sunNow` был мёртвым методом (никто не звал) и снят; путь солнца
// читает состояние через `_renderSunRays`, который остаётся в списке.
for (const name of [
'_roomLqi', '_resolvedRoomFills', '_sunNow', '_renderSunRays', '_renderGlowLayer',
'_roomLqi', '_resolvedRoomFills', '_renderSunRays', '_renderGlowLayer',
'_renderVacuums', '_renderVacFit', '_renderDevice', '_roomTemp', '_roomHum', '_openingAmt',
'_renderOpeningLocks', '_renderDecorLayer',
]) {