mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-08 07:29:13 +00:00
Compare commits
521
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e8ea4685 | ||
|
|
ee351dcb84 | ||
|
|
2b841d94e4 | ||
|
|
c175a2ab1a | ||
|
|
020739ebca | ||
|
|
ffdd69e277 | ||
|
|
319ee70010 | ||
|
|
5c893c7ff3 | ||
|
|
077e9c1b1b | ||
|
|
0471482c2e | ||
|
|
db887cd7f0 | ||
|
|
7feda5b6c8 | ||
|
|
285a6dc436 | ||
|
|
52876b0573 | ||
|
|
cdfe02e64d | ||
|
|
07c885435a | ||
|
|
67af859e8c | ||
|
|
4a7abf531b | ||
|
|
379de706d2 | ||
|
|
1faed2af7d | ||
|
|
343449e694 | ||
|
|
9722153e01 | ||
|
|
0f77b921f6 | ||
|
|
e69b3f2009 | ||
|
|
7341ecf9c5 | ||
|
|
fd406acea2 | ||
|
|
33022ff946 | ||
|
|
55ac10be3c | ||
|
|
22828495bb | ||
|
|
370c082f31 | ||
|
|
0d64547643 | ||
|
|
2ac221d28b | ||
|
|
d19bac3966 | ||
|
|
aef02229c6 | ||
|
|
865312e0b0 | ||
|
|
841bad4e79 | ||
|
|
9ebc430983 | ||
|
|
0a098c1e51 | ||
|
|
cf1c90fb5b | ||
|
|
7fb20c658b | ||
|
|
05c8e164c2 | ||
|
|
9deb1dcfd6 | ||
|
|
a5e06b0527 | ||
|
|
14f5b90c24 | ||
|
|
4de49e0db2 | ||
|
|
80cba30895 | ||
|
|
96f89717bd | ||
|
|
d8949cfffc | ||
|
|
7777b8a0a8 | ||
|
|
45aeadb38e | ||
|
|
b07ef67dcb | ||
|
|
d58a5d0dad | ||
|
|
181b61ae8e | ||
|
|
24e48935c3 | ||
|
|
6b0d7d66dd | ||
|
|
0588fa8f0a | ||
|
|
019cfa5c5a | ||
|
|
5fff48f7b1 | ||
|
|
a74a50d331 | ||
|
|
416d2fae71 | ||
|
|
08bc58cc0e | ||
|
|
c142e4f7b5 | ||
|
|
ad10a5b099 | ||
|
|
76310fc981 | ||
|
|
0f466743c3 | ||
|
|
35ef231191 | ||
|
|
fab28af26f | ||
|
|
a54429d4a3 | ||
|
|
9598768167 | ||
|
|
aa1105468b | ||
|
|
42d1a8a9dc | ||
|
|
8b88a6f606 | ||
|
|
4c7e9815f3 | ||
|
|
1deb29e163 | ||
|
|
3048ac9195 | ||
|
|
654cb9d404 | ||
|
|
8dbfe5e6b2 | ||
|
|
706eb02d70 | ||
|
|
4962de1d5c | ||
|
|
31b4ddda55 | ||
|
|
072fb108cf | ||
|
|
24671d4358 | ||
|
|
6607a34973 | ||
|
|
e9e6eba5d1 | ||
|
|
a20ef2b9b0 | ||
|
|
e447364636 | ||
|
|
b40e8ff58d | ||
|
|
ef3e7a5440 | ||
|
|
403bcca680 | ||
|
|
91b57ae60e | ||
|
|
3d2c5ab3ee | ||
|
|
5d3f29c249 | ||
|
|
ec9d968c8a | ||
|
|
8915e49a58 | ||
|
|
d515c42177 | ||
|
|
ade3078d53 | ||
|
|
d21fba11d6 | ||
|
|
ed238059e1 | ||
|
|
8237bbc1cc | ||
|
|
05d62f053a | ||
|
|
3a1cd7b1ad | ||
|
|
fb3071d3c1 | ||
|
|
870237f3fb | ||
|
|
9fa5efbdac | ||
|
|
73b0cfff5b | ||
|
|
13d8df5554 | ||
|
|
9e1a2f4a07 | ||
|
|
16069d1dcc | ||
|
|
23f822475b | ||
|
|
ceee249210 | ||
|
|
d0c13bc555 | ||
|
|
11e9eb10df | ||
|
|
044f09a6a5 | ||
|
|
92b89d9b80 | ||
|
|
2bcde13158 | ||
|
|
7bbab4b61a | ||
|
|
9882046b86 | ||
|
|
a86be350b1 | ||
|
|
3d68a2fe23 | ||
|
|
2d7a1c9788 | ||
|
|
bb9aaefe2a | ||
|
|
6ed9aa1652 | ||
|
|
f5b20e07f6 | ||
|
|
c4cb0649ae | ||
|
|
baf0136d9a | ||
|
|
6149bcf866 | ||
|
|
e2d38d4e16 | ||
|
|
f2936813af | ||
|
|
a05458b6f3 | ||
|
|
5021c48446 | ||
|
|
8a3bf71ef1 | ||
|
|
97d40ea710 | ||
|
|
8e4f417f51 | ||
|
|
b8f662cdf1 | ||
|
|
d709ea110d | ||
|
|
9f695d8a5d | ||
|
|
0606a3664a | ||
|
|
ae3db2bacd | ||
|
|
c9bf16dec3 | ||
|
|
94bd932153 | ||
|
|
134a7029b4 | ||
|
|
cf7a9cc4fc | ||
|
|
146da62b0d | ||
|
|
cdfc6e59dc | ||
|
|
09fb02cf15 | ||
|
|
dd86bf955a | ||
|
|
97f38ecaae | ||
|
|
50b163c60c | ||
|
|
1f15ee143e | ||
|
|
0d989b79e2 | ||
|
|
a6076e585e | ||
|
|
a2578e6f14 | ||
|
|
b76e0d38ef | ||
|
|
b18d366e8c | ||
|
|
134e3e79da | ||
|
|
30508e1a5c | ||
|
|
3c58ad1e73 | ||
|
|
f4ec9d995b | ||
|
|
ff4e096858 | ||
|
|
5c0fe8e79f | ||
|
|
bf0bc1f4a6 | ||
|
|
04084581c0 | ||
|
|
341c9fcfce | ||
|
|
b7dbc21c42 | ||
|
|
14ea266112 | ||
|
|
255677556f | ||
|
|
953f15b967 | ||
|
|
c86e4bdd31 | ||
|
|
24ea768609 | ||
|
|
30dcc5b2cb | ||
|
|
a22f6f5411 | ||
|
|
aa910c31a3 | ||
|
|
72975e7514 | ||
|
|
25001ef7ab | ||
|
|
d93bcf2628 | ||
|
|
09bf6a95ec | ||
|
|
de3e1f00e1 | ||
|
|
653d94ef09 | ||
|
|
475f391cf9 | ||
|
|
3078962b64 | ||
|
|
6abb589f0c | ||
|
|
4aa6c85f98 | ||
|
|
9deed2a3c3 | ||
|
|
80b63b7ee8 | ||
|
|
024583bd22 | ||
|
|
f0e1c0b8cc | ||
|
|
86a301f618 | ||
|
|
1abd65a2be | ||
|
|
62a2e3b05b | ||
|
|
fa18ca81c9 | ||
|
|
ffa13c6628 | ||
|
|
5e1b508215 | ||
|
|
88702eedc6 | ||
|
|
d4eea609cf | ||
|
|
dc77852e0b | ||
|
|
f10c5b4374 | ||
|
|
9e64fc80ee | ||
|
|
d8e09cd1a0 | ||
|
|
da8edb1f0e | ||
|
|
1265036d2a | ||
|
|
a7b02db609 | ||
|
|
8070357266 | ||
|
|
2095a40f41 | ||
|
|
9c4a45a6b2 | ||
|
|
f5a6b47b39 | ||
|
|
707cb18f7e | ||
|
|
929044b932 | ||
|
|
4091d83af3 | ||
|
|
b84465e541 | ||
|
|
7ce3f6629f | ||
|
|
33a787a151 | ||
|
|
a0f72280ee | ||
|
|
88985d2e4c | ||
|
|
c629c9c238 | ||
|
|
9324d81b5f | ||
|
|
37238f3990 | ||
|
|
5a49c8c32d | ||
|
|
8b7725acb1 | ||
|
|
c84c430b2e | ||
|
|
0e44f8a69f | ||
|
|
7ff2b5aeaa | ||
|
|
4097132374 | ||
|
|
a729b5000d | ||
|
|
36ebabaaec | ||
|
|
de117d5042 | ||
|
|
6372c21eec | ||
|
|
5e31816881 | ||
|
|
b417ec4c9e | ||
|
|
15f8da8563 | ||
|
|
d87f834448 | ||
|
|
d5bdfde919 | ||
|
|
f559b3fe19 | ||
|
|
90ea1cab84 | ||
|
|
9debce3394 | ||
|
|
17b7b0ad73 | ||
|
|
7cb90e379b | ||
|
|
df8482c702 | ||
|
|
82fbad67d5 | ||
|
|
bc59917ea4 | ||
|
|
c39a8fe88e | ||
|
|
b0751497d4 | ||
|
|
c2ca6a55d2 | ||
|
|
1c49544831 | ||
|
|
b998b0b34a | ||
|
|
712d41b6d6 | ||
|
|
76558bf2a0 | ||
|
|
0621d53a49 | ||
|
|
bb25110dc4 | ||
|
|
f3cfb93e94 | ||
|
|
b27ae06ef8 | ||
|
|
7126fd8a02 | ||
|
|
02dce77c98 | ||
|
|
53bb3bf453 | ||
|
|
0ab685e380 | ||
|
|
4eb712be0e | ||
|
|
8e42f4e972 | ||
|
|
562313944f | ||
|
|
3b9f25ea65 | ||
|
|
cf8b8f0307 | ||
|
|
d5bca5dee5 | ||
|
|
61d98158ce | ||
|
|
e58d7d06f8 | ||
|
|
af339fda3b | ||
|
|
011e351249 | ||
|
|
235f60e693 | ||
|
|
8fac66e1c3 | ||
|
|
530eb0090d | ||
|
|
d327ec3d93 | ||
|
|
56be779ca7 | ||
|
|
5e430d9ede | ||
|
|
0cc7c60e8b | ||
|
|
b3dd9444b5 | ||
|
|
52dc08a0e5 | ||
|
|
78acf2c6ba | ||
|
|
dcb8f80de2 | ||
|
|
6f17b6cd4c | ||
|
|
153fa7a680 | ||
|
|
fc835cb97d | ||
|
|
499c0860df | ||
|
|
8df4ef710c | ||
|
|
1d51beade1 | ||
|
|
a49f7095ce | ||
|
|
3847baa5a0 | ||
|
|
5ce83feffa | ||
|
|
fd082ad460 | ||
|
|
85598638c1 | ||
|
|
0f7ec02352 | ||
|
|
715735d753 | ||
|
|
40607aa37f | ||
|
|
34423a6236 | ||
|
|
295d61f377 | ||
|
|
5f8e8ca7e8 | ||
|
|
108427dc77 | ||
|
|
3e7f7d3936 | ||
|
|
0d85807157 | ||
|
|
5cd9d0419a | ||
|
|
e00f62dca0 | ||
|
|
bbcc88caf1 | ||
|
|
66ac7d23ec | ||
|
|
2d735c56f9 | ||
|
|
48b001c646 | ||
|
|
dccbb80890 | ||
|
|
d11ad9c1c2 | ||
|
|
7188db8187 | ||
|
|
a5a73d1511 | ||
|
|
a312897527 | ||
|
|
7ac24a03a0 | ||
|
|
7574518575 | ||
|
|
12bdc0be68 | ||
|
|
8b7628285a | ||
|
|
37b1cbf74b | ||
|
|
40b226405d | ||
|
|
2f1be1a86a | ||
|
|
24cb6d113a | ||
|
|
6e2c75afc9 | ||
|
|
e5c217111c | ||
|
|
84ed38e3d5 | ||
|
|
2022e183a5 | ||
|
|
3e38de07b3 | ||
|
|
aee0a5825b | ||
|
|
3d99f261ff | ||
|
|
204e728097 | ||
|
|
1557475af3 | ||
|
|
445a9a0cdf | ||
|
|
07d90be28f | ||
|
|
f56342479b | ||
|
|
bcff8c7157 | ||
|
|
a4af5a2008 | ||
|
|
9e1cc91277 | ||
|
|
dca0fd2876 | ||
|
|
838a60b3b0 | ||
|
|
2606fffa97 | ||
|
|
8afaa63caf | ||
|
|
5c0e8668f1 | ||
|
|
80e3dba700 | ||
|
|
c2c806fae3 | ||
|
|
6da6d9c699 | ||
|
|
0d641ffbfd | ||
|
|
e19f5b0a0e | ||
|
|
90d38b1685 | ||
|
|
683c9250b9 | ||
|
|
2bc688e6c3 | ||
|
|
349af7da66 | ||
|
|
59e05482fe | ||
|
|
c8c470ed57 | ||
|
|
e515bdaab8 | ||
|
|
a352c7c132 | ||
|
|
a372b354c6 | ||
|
|
9caa3c2316 | ||
|
|
df20da9676 | ||
|
|
bb33626f38 | ||
|
|
4c74e80aa9 | ||
|
|
eaeada16ae | ||
|
|
09d35c8d2c | ||
|
|
920e626948 | ||
|
|
a8321e32cc | ||
|
|
18c9f8e77c | ||
|
|
674e589ad8 | ||
|
|
26763c7ca4 | ||
|
|
52a56430ab | ||
|
|
6e07313460 | ||
|
|
f903e93c83 | ||
|
|
ae0e516af1 | ||
|
|
e7fca7b955 | ||
|
|
48f6f2e6a7 | ||
|
|
c949e0bbc9 | ||
|
|
5bb7197620 | ||
|
|
6a8658cab9 | ||
|
|
258d1990c8 | ||
|
|
2369c50607 | ||
|
|
c68d92f674 | ||
|
|
8bb43fd379 | ||
|
|
310bc6e4f8 | ||
|
|
1aa52d2107 | ||
|
|
f6e317d871 | ||
|
|
a005aae5b6 | ||
|
|
e45bc87c6d | ||
|
|
e1700757de | ||
|
|
1606407107 | ||
|
|
8dcc1cad4e | ||
|
|
224d0106fd | ||
|
|
ee3aa068fc | ||
|
|
19dc61db15 | ||
|
|
c66884b7f5 | ||
|
|
59bf216b60 | ||
|
|
6ab791e348 | ||
|
|
e6dfa9a752 | ||
|
|
5986332eda | ||
|
|
13054e246e | ||
|
|
fa570642df | ||
|
|
2a62ad5b95 | ||
|
|
c716bb0f63 | ||
|
|
ac75df7e9a | ||
|
|
e1ae8f4ac7 | ||
|
|
9e4bfb4379 | ||
|
|
d8465b3a09 | ||
|
|
f6c76b7351 | ||
|
|
51854aea53 | ||
|
|
57ce10721f | ||
|
|
7d4d75bdfb | ||
|
|
9e5f211b0a | ||
|
|
1ae0ea1c3b | ||
|
|
db2224c964 | ||
|
|
4b23679d3c | ||
|
|
f2b66ad677 | ||
|
|
836ef7710f | ||
|
|
87195c2b5d | ||
|
|
600330e187 | ||
|
|
c1e05e465b | ||
|
|
0a09712bc5 | ||
|
|
33ae14a99d | ||
|
|
09f8cf4c11 | ||
|
|
53503e0373 | ||
|
|
37fbb9824d | ||
|
|
3f17a1497d | ||
|
|
20f17fee54 | ||
|
|
1751184b12 | ||
|
|
63c24784b6 | ||
|
|
8207a175d6 | ||
|
|
c04d0778ae | ||
|
|
e44fbd6c9e | ||
|
|
c047ca2230 | ||
|
|
cd59b2eb66 | ||
|
|
52b1b2e7c9 | ||
|
|
52fe4d6499 | ||
|
|
005b7c406d | ||
|
|
0a5b34a571 | ||
|
|
c37b5b92bc | ||
|
|
139dceaa4f | ||
|
|
13af1d5e30 | ||
|
|
e8e7ecc9f2 | ||
|
|
1493145ba5 | ||
|
|
b1bbf4b076 | ||
|
|
128af2392a | ||
|
|
560849a1a7 | ||
|
|
cab4b4153a | ||
|
|
a0b471ca1b | ||
|
|
0ff908690a | ||
|
|
e79f3cbfbb | ||
|
|
ffcdb2945f | ||
|
|
67ac9981cf | ||
|
|
c455e7c82d | ||
|
|
03a3dd9916 | ||
|
|
6d8d077340 | ||
|
|
0dac3488fb | ||
|
|
0991c45374 | ||
|
|
65b3cb25f7 | ||
|
|
cca9bc856b | ||
|
|
df46fd1c3e | ||
|
|
d4a672c715 | ||
|
|
c64a1ab7d5 | ||
|
|
d460b31120 | ||
|
|
f880db0338 | ||
|
|
2f86266f60 | ||
|
|
d5f3e17f24 | ||
|
|
dd365cff45 | ||
|
|
20d2863eea | ||
|
|
2c9ff32dc6 | ||
|
|
1a7b1dfceb | ||
|
|
64b544f168 | ||
|
|
5b5b808184 | ||
|
|
cf01489fa9 | ||
|
|
dd3a50426d | ||
|
|
bfcd9f1f15 | ||
|
|
72ce15bfa1 | ||
|
|
fdde46541e | ||
|
|
4bc3e9baa6 | ||
|
|
696f5a789f | ||
|
|
972ff701d1 | ||
|
|
a57ba2e264 | ||
|
|
708ed850f3 | ||
|
|
c8fc8838d8 | ||
|
|
f4fc3bd680 | ||
|
|
030780386c | ||
|
|
5a258f3128 | ||
|
|
f989fbfe3d | ||
|
|
15ef7d9700 | ||
|
|
58211d8f64 | ||
|
|
bd949a810f | ||
|
|
174ececb28 | ||
|
|
614310a1e7 | ||
|
|
9a1d922b24 | ||
|
|
c081f59a15 | ||
|
|
16cf433164 | ||
|
|
e25702c8d6 | ||
|
|
dd905888cb | ||
|
|
ab89187f4f | ||
|
|
69e0345122 | ||
|
|
89e8aecbc1 | ||
|
|
93051edfc2 | ||
|
|
062af8f1d9 | ||
|
|
08b1c4820b | ||
|
|
dd08aed99e | ||
|
|
cc71918126 | ||
|
|
718afca221 | ||
|
|
b9aa2b54b1 | ||
|
|
196be82d94 | ||
|
|
f340157f59 | ||
|
|
7acf5c74ad | ||
|
|
075a2d3fb1 | ||
|
|
7a4032ba26 | ||
|
|
9a7020ce42 | ||
|
|
19edda83e3 | ||
|
|
9fe7f88cb3 | ||
|
|
c392ad3a9c | ||
|
|
f1a87fbad9 | ||
|
|
ac8eac6034 | ||
|
|
bdac4b4fdc | ||
|
|
6fc789e4c8 | ||
|
|
0a066177cf | ||
|
|
85cba3d8b4 | ||
|
|
cbc39cea7b | ||
|
|
d24b0f0eac | ||
|
|
dc72141af6 | ||
|
|
8741f3f6d7 | ||
|
|
be54edf988 | ||
|
|
7752329d71 | ||
|
|
093ea906ae | ||
|
|
0c93344913 | ||
|
|
eecb5291d0 | ||
|
|
58ba44dbac |
@@ -12,3 +12,9 @@
|
||||
*.zip binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
|
||||
# #698: записи двух задач в `## Unreleased` друг другу не противоречат — при
|
||||
# слиянии и ребейзе git объединяет обе стороны сам (встроенный драйвер union),
|
||||
# и параллельные задачи не возвращаются в S6 из-за соседней строки ченджлога.
|
||||
docs/CHANGELOG.md merge=union
|
||||
docs/CHANGELOG.ru.md merge=union
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
name: "Бета: производные артефакты на dev · тело (#716)"
|
||||
|
||||
# #697, PROCESS.md §8 и §11.4: ветки задач не коммитят ни отпечаток и кадры
|
||||
# скриншотов документации, ни эталоны golden. Их обновляет один коммит бота на
|
||||
# `dev` перед кандидатом беты; коммит проверяет релиз-менеджер.
|
||||
#
|
||||
# - Скриншоты: съёмка на `dev` тем же каноническим способом, что
|
||||
# `docs-screenshots.yml`, и приёмка `docs:accept --reviewed`. Кадры, которые
|
||||
# менять не собирались, обязаны совпасть байт-в-байт (свидетели среды);
|
||||
# изменившийся кадр принимается, только если назван в `docs_expect_change`.
|
||||
# Ничего не изменилось — обновляется один отпечаток исходников.
|
||||
# - Golden: артефакт `golden-images` полного Validate на `dev`
|
||||
# (`golden_run`) и `golden:accept --reviewed` с объявленными сценами. Коммит
|
||||
# несёт `Release:` и `Baseline-Reviewed:` — провенанс, который требует
|
||||
# validate-commit-provenance.mjs.
|
||||
#
|
||||
# Необъявленная разница — отказ с перечнем кадров, артефакт съёмки остаётся
|
||||
# для просмотра: решение, какие кадры сдвинулись законно, принимает человек.
|
||||
|
||||
on:
|
||||
# #716: тело вызывается тонким файлом `beta-derived.yml` по ссылке `@dev`;
|
||||
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
|
||||
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
|
||||
required: true
|
||||
type: string
|
||||
docs_expect_change:
|
||||
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
golden_run:
|
||||
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
golden_expect_change:
|
||||
description: "Golden scenes expected to change, comma-separated"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
golden_expect_new:
|
||||
description: "New golden scenes, comma-separated"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Concurrency уровня workflow — у вызывающего `beta-derived.yml` (#716).
|
||||
|
||||
jobs:
|
||||
accept:
|
||||
name: "Отпечаток, кадры и эталоны — одним коммитом в dev"
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 40
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
- name: Install pinned Chromium
|
||||
if: steps.pw.outputs.cache-hit != 'true'
|
||||
run: npx playwright install chromium
|
||||
- name: Build the bundle the screenshots must come from
|
||||
run: npm run build
|
||||
# Тот же закреплённый упаковщик, что у docs-screenshots.yml: другой
|
||||
# переписал бы все кадры без единого изменённого пикселя.
|
||||
- name: Установить oxipng
|
||||
env:
|
||||
OXIPNG_VERSION: 10.2.0
|
||||
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
|
||||
run: |
|
||||
set -euo pipefail
|
||||
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
curl -fsSL -o "$asset" \
|
||||
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
|
||||
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
|
||||
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/oxipng" --version
|
||||
- name: "Съёмка воспроизводима между прогонами (#410, #422)"
|
||||
run: node scripts/capture-determinism.mjs
|
||||
- name: Кадры документации — съёмка и приёмка
|
||||
id: docs
|
||||
env:
|
||||
EXPECT: ${{ inputs.docs_expect_change }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node demo/docs/capture.mjs --stability=3
|
||||
cand="$RUNNER_TEMP/docs-candidate"
|
||||
rm -rf "$cand" && mkdir -p "$cand"
|
||||
cp docs/images/*.png docs/images/screenshots.json "$cand/"
|
||||
# Приёмка сравнивает кандидата с ЗАКОММИЧЕННЫМИ кадрами: рабочая
|
||||
# копия возвращается к dev, и заменить файлы может только она.
|
||||
git checkout -- docs/images
|
||||
git clean -fdq -- docs/images
|
||||
args=(--reviewed "--from=$cand")
|
||||
if [ -n "$EXPECT" ]; then args+=("--expect-change=$EXPECT"); fi
|
||||
node scripts/docs-accept.mjs "${args[@]}"
|
||||
if git diff --quiet -- docs/images; then changed=false; else changed=true; fi
|
||||
echo "changed=$changed" >> "$GITHUB_OUTPUT"
|
||||
git diff --stat -- docs/images
|
||||
- name: Сохранить кандидата скриншотов для просмотра
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: docs-candidate-${{ github.run_id }}
|
||||
path: ${{ runner.temp }}/docs-candidate
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
- name: Эталоны golden из прогона Validate
|
||||
id: golden
|
||||
if: inputs.golden_run != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN: ${{ inputs.golden_run }}
|
||||
EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
|
||||
EXPECT_NEW: ${{ inputs.golden_expect_new }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$RUN" in ''|*[!0-9]*) echo "::error::golden_run — числовой id прогона Validate"; exit 1 ;; esac
|
||||
meta=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN" \
|
||||
--jq '[.path, .head_branch, .status, .html_url] | @tsv')
|
||||
IFS=$'\t' read -r path branch status url <<< "$meta"
|
||||
# Провенанс эталонов — завершённый Validate на dev: другой workflow
|
||||
# или ветка задачи доказательством для кандидата беты не являются.
|
||||
if [ "$path" != ".github/workflows/validate.yml" ] || [ "$branch" != "dev" ] || [ "$status" != "completed" ]; then
|
||||
echo "::error::прогон $RUN — не завершённый Validate на dev ($path, $branch, $status)"
|
||||
exit 1
|
||||
fi
|
||||
from="$RUNNER_TEMP/golden-candidate"
|
||||
rm -rf "$from" && mkdir -p "$from"
|
||||
gh run download "$RUN" --repo "${{ github.repository }}" -n golden-images -D "$from"
|
||||
# Отчёт обязан быть снят с этого же дерева: accept.mjs сверяет его
|
||||
# отпечаток с исходниками и откажет, если dev ушёл вперёд.
|
||||
args=(--reviewed "--from=$from")
|
||||
if [ -n "$EXPECT_CHANGE" ]; then args+=("--expect-change=$EXPECT_CHANGE"); fi
|
||||
if [ -n "$EXPECT_NEW" ]; then args+=("--expect-new=$EXPECT_NEW"); fi
|
||||
node scripts/golden-accept.mjs "${args[@]}"
|
||||
if git diff --quiet -- demo/golden/baselines; then changed=false; else changed=true; fi
|
||||
{ echo "changed=$changed"; echo "url=$url"; } >> "$GITHUB_OUTPUT"
|
||||
git diff --stat -- demo/golden/baselines
|
||||
- name: Коммит в dev
|
||||
env:
|
||||
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
DOCS_CHANGED: ${{ steps.docs.outputs.changed }}
|
||||
DOCS_EXPECT: ${{ inputs.docs_expect_change }}
|
||||
GOLDEN_CHANGED: ${{ steps.golden.outputs.changed }}
|
||||
GOLDEN_URL: ${{ steps.golden.outputs.url }}
|
||||
GOLDEN_EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
|
||||
GOLDEN_EXPECT_NEW: ${{ inputs.golden_expect_new }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
# Хук pre-push гоняет process-gate; gate:small здесь не нужен —
|
||||
# продуктового кода коммит не несёт.
|
||||
HP_PREPUSH_GATE: "0"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add -- docs/images demo/golden/baselines
|
||||
if git diff --cached --quiet; then
|
||||
echo "::notice::отпечаток, кадры и эталоны на dev уже свежие — коммитить нечего"
|
||||
exit 0
|
||||
fi
|
||||
msg="$RUNNER_TEMP/message.txt"
|
||||
{
|
||||
echo "docs: accept derived artifacts on dev for $TAG"
|
||||
echo ""
|
||||
echo "Производные артефакты беты — одним коммитом на dev (PROCESS.md §8, §11.4, #697)."
|
||||
echo "Прогон: $RUN_URL"
|
||||
if [ "$DOCS_CHANGED" = "true" ]; then
|
||||
echo "Скриншоты документации: отпечаток исходников; изменённые кадры: ${DOCS_EXPECT:-нет}."
|
||||
fi
|
||||
if [ "$GOLDEN_CHANGED" = "true" ]; then
|
||||
echo "Golden: изменённые сцены: ${GOLDEN_EXPECT_CHANGE:-нет}; новые: ${GOLDEN_EXPECT_NEW:-нет}."
|
||||
fi
|
||||
echo ""
|
||||
if [ "$GOLDEN_CHANGED" = "true" ]; then
|
||||
echo "Release: $TAG"
|
||||
echo "Baseline-Reviewed: $GOLDEN_URL"
|
||||
fi
|
||||
echo "Issue: #697"
|
||||
echo "User-Visible: no"
|
||||
} > "$msg"
|
||||
git -c user.name="claude[bot]" \
|
||||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||||
commit -q -F "$msg"
|
||||
git show --stat --format='%H %s' HEAD | head -40
|
||||
# #730: сдвигом dev считается только устаревший lease — отказ
|
||||
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705).
|
||||
# Скрипт — из dev: рабочая копия и есть dev. Сдвиг — прежний совет
|
||||
# перезапустить; отказ GitHub (право на workflow, правило ветки, хук)
|
||||
# перезапуск не лечит: причина и ответ git без токена — в журнале и в
|
||||
# сводке шага.
|
||||
push_err="$RUNNER_TEMP/beta-derived-push.stderr"
|
||||
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
|
||||
--stage=beta-derived --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||||
if [ "$kind" = "stale" ]; then
|
||||
echo "::error::dev ушёл вперёд за время съёмки — запустить workflow заново: отпечаток судит дерево, а оно уже другое"
|
||||
else
|
||||
echo "::error::push производных артефактов в dev отклонён ($kind) — это не сдвиг dev, перезапуск не поможет; причина и ответ git — выше и в сводке шага"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
echo "### Производные артефакты $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Коммит \`$(git rev-parse --short HEAD)\` в dev — проверить перед кандидатом беты." >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -8,15 +8,14 @@ name: "Мутационный гейт · тело (#623)"
|
||||
# Прогон дорогой и проверяет не продукт, а тесты, поэтому он не входит ни в
|
||||
# Validate, ни в цикл разработки, ни в релизный гейт (#513, решение владельца
|
||||
# 09.09): каждую ночь по расписанию, отказ — issue с отчётом (#472).
|
||||
# Мутанты, задетые диффом, конвейер ревью и слияние гоняют отдельно на своём
|
||||
# кандидате (#510); кандидат беты, `full=true` и ночной Validate их не
|
||||
# запрашивают (#601) — ночью достаточно этого полного реестра.
|
||||
# С #709 ревью, слияние, кандидат беты, `full=true` и ночной Validate
|
||||
# мутантов не запрашивают — ночью достаточно этого полного реестра.
|
||||
# Дешёвая половина — «якоря патчей живы, guard-файлы существуют» — идёт с
|
||||
# обычными юнитами: test/mutation-gate.test.mjs.
|
||||
#
|
||||
# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle),
|
||||
# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный
|
||||
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
|
||||
# tsc), а реестр режется на чересполосные шарды — полный прогон
|
||||
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
|
||||
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
|
||||
#
|
||||
@@ -52,7 +51,8 @@ jobs:
|
||||
# commit/tree, а не самостоятельно читают dev в разное время.
|
||||
material:
|
||||
name: "Зафиксировать неизменяемый материал"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
sha: ${{ steps.identity.outputs.sha }}
|
||||
tree: ${{ steps.identity.outputs.tree }}
|
||||
@@ -109,20 +109,20 @@ jobs:
|
||||
fi
|
||||
|
||||
mutants:
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 10)"
|
||||
needs: material
|
||||
if: needs.material.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
# Шесть чересполосных шардов (#604): при четырёх шард нёс ~203 мутанта из
|
||||
# 810 и рос с реестром — 42 мин 10.09, 57 мин 20.09, 61 мин 21.09, и шард
|
||||
# 2/4 был снят по потолку без единого FAIL. Делитель тот же, что у
|
||||
# `changed_mutants` в Validate; число шардов повторяется в `--shard=i/6`,
|
||||
# `--shards=6` и имени job — тест `mutation-gate.test.mjs` держит их
|
||||
# равными. Бандл собирают только браузерные гварды. Час — потолок против
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
# #795: ночь 05.10 (1164 мутанта) заняла 42–60 мин на шести шардах;
|
||||
# четвёртый отменён по потолку. Десять шардов оставляют 116–117 мутантов
|
||||
# вместо 194. Оценка по длительностям той ночи — максимум около 39 мин
|
||||
# с запасом на setup; это прогноз, фактическое время проверяет следующая ночь.
|
||||
# Матрицу, делители runner/evidence и имя job держит mutation-gate.test.mjs.
|
||||
# Бандл собирают только браузерные гварды. Час — потолок против
|
||||
# зависшего Chromium, а не бюджет шарда: шард, упёршийся в него, — сигнал
|
||||
# снова делить, и отчёт (#472) называет такой шард прерванным, не «ok».
|
||||
timeout-minutes: 60
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p artifacts/mutation-shard-${{ matrix.shard }}
|
||||
set -o pipefail
|
||||
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/6 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}/mutation-shard-${{ matrix.shard }}.log
|
||||
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/10 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}/mutation-shard-${{ matrix.shard }}.log
|
||||
# Исход шага едет в evidence (#604): снятый по timeout-minutes шаг даёт
|
||||
# `cancelled`, и агрегатор отвергает такой шард как неполный — лог без
|
||||
# строк FAIL сам по себе зелёным не считается.
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--workflow-sha=${{ job.workflow_sha }} \
|
||||
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
|
||||
--shard=${{ matrix.shard }} --shards=6 \
|
||||
--shard=${{ matrix.shard }} --shards=10 \
|
||||
--outcome=${{ steps.gate.outcome }}
|
||||
- name: Сохранить лог и identity шарда
|
||||
if: always()
|
||||
@@ -201,14 +201,15 @@ jobs:
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# Результат нельзя приписывать material, пока не доказаны все шесть шардов —
|
||||
# Результат нельзя приписывать material, пока не доказаны все десять шардов —
|
||||
# каждый с identity и с дошедшим до конца прогоном (#549, #604).
|
||||
# always() нужен при красном мутанте: лог красного шарда всё равно evidence.
|
||||
evidence:
|
||||
name: "Доказать единый material всех шардов"
|
||||
needs: [material, mutants]
|
||||
if: always() && needs.material.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -226,20 +227,21 @@ jobs:
|
||||
- name: Проверить полноту и identity
|
||||
run: |
|
||||
node scripts/mutation-gate-report.mjs --verify-only \
|
||||
--logs=artifacts/mutation-logs --shards=6 \
|
||||
--logs=artifacts/mutation-logs --shards=10 \
|
||||
--sha=${{ needs.material.outputs.sha }} \
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--workflow-sha=${{ job.workflow_sha }} \
|
||||
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }}
|
||||
|
||||
# #620: маркер пишется ТОЛЬКО после зелёного агрегатора — все шесть шардов
|
||||
# #620: маркер пишется ТОЛЬКО после зелёного агрегатора — все десять шардов
|
||||
# доказаны на одном material. Красный или неполный прогон маркера не оставляет,
|
||||
# и следующая ночь гонит реестр заново.
|
||||
green_marker:
|
||||
name: "Записать маркер зелёного прогона"
|
||||
needs: [material, mutants, evidence]
|
||||
if: needs.material.outputs.reuse != 'true' && needs.mutants.result == 'success' && needs.evidence.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -275,7 +277,8 @@ jobs:
|
||||
# является — её шарды пропущены намеренно. Любой другой пропуск шардов
|
||||
# (упал material) по-прежнему заводит issue.
|
||||
if: always() && github.event_name == 'schedule' && needs.material.outputs.reuse != 'true' && (needs.mutants.result != 'success' || needs.evidence.result != 'success')
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -302,7 +305,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
node scripts/mutation-gate-report.mjs \
|
||||
--require-evidence --logs=artifacts/mutation-logs --shards=6 \
|
||||
--require-evidence --logs=artifacts/mutation-logs --shards=10 \
|
||||
--sha=${{ needs.material.outputs.sha }} \
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--workflow-sha=${{ job.workflow_sha }} \
|
||||
|
||||
@@ -12,12 +12,19 @@
|
||||
#
|
||||
# Красный ночной прогон — сигнал автору последних коммитов на dev, не гейт:
|
||||
# гейт беты по-прежнему требует зелёный Validate на точном SHA кандидата, и
|
||||
# там полный набор идёт заново.
|
||||
# там полный набор идёт заново. Адресат сигнала (#736) — задачи, чьи коммиты
|
||||
# классов A/B вошли в dev после последней зелёной ночи: job `night_red` пишет
|
||||
# каждой один комментарий (`scripts/night-red.mjs`, PROCESS.md §10.4).
|
||||
#
|
||||
# Сигнал обязан быть настоящим (#492 §7): до этой задачи job завершалась
|
||||
# успехом в момент постановки Validate в очередь, и красный полный прогон не
|
||||
# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт
|
||||
# его: успешный dispatch — не успешная проверка.
|
||||
#
|
||||
# #727: после Validate при любом его исходе ночь запускает пакетное ревью ship
|
||||
# (`ship-review.yml -f tag=nightly`) на SHA прогона Validate: оно читает код, а
|
||||
# не прогон. Ship-задача узнаёт о дефектах в ночь после слияния, а бета читает
|
||||
# только то, что ночь не прочла (PROCESS.md §11.7). Цвет ночи — цвет Validate.
|
||||
name: "Ночной полный прогон dev · тело (#623)"
|
||||
|
||||
on:
|
||||
@@ -32,10 +39,37 @@ permissions:
|
||||
jobs:
|
||||
dispatch:
|
||||
name: "Запустить Validate на dev с полным набором и дождаться результата"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
outputs:
|
||||
# #727: SHA прогона Validate — отдельным шагом до ожидания, красный Validate его не теряет.
|
||||
head_sha: ${{ steps.validate.outputs.head_sha }}
|
||||
# #736: прогон Validate для разбора красной ночи — тем же шагом, до ожидания.
|
||||
run_id: ${{ steps.validate.outputs.run_id }}
|
||||
steps:
|
||||
- env:
|
||||
# #658: при плане 02:30 UTC ночь фактически стартовала в 07:42–08:05 и
|
||||
# кончалась в рабочее время владельца. Сдвиг старта больше часа — видимое
|
||||
# предупреждение, а не молча съеденная ночь. `github.event.schedule` —
|
||||
# строка cron вызывающего `nightly.yml`; NOW_EPOCH подставляет только тест.
|
||||
- name: "Сдвиг старта ночи против расписания"
|
||||
if: github.event_name == 'schedule'
|
||||
env:
|
||||
SCHEDULE: ${{ github.event.schedule }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
read -r minute hour _ <<< "$SCHEDULE"
|
||||
now=${NOW_EPOCH:-$(date -u +%s)}
|
||||
planned=$(date -u -d "@$now" +%F)
|
||||
planned=$(date -u -d "$planned $hour:$minute" +%s)
|
||||
[ "$planned" -le "$now" ] || planned=$((planned - 86400))
|
||||
lag=$(( (now - planned) / 60 ))
|
||||
echo "- Старт по расписанию \`$SCHEDULE\` (UTC): сдвиг $lag мин" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$lag" -gt 60 ]; then
|
||||
echo "::warning::ночной прогон стартовал на $lag мин позже расписания ($SCHEDULE UTC) — очередь расписаний GitHub (#658)"
|
||||
fi
|
||||
- name: "Запустить Validate и найти его прогон"
|
||||
id: validate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
@@ -60,5 +94,107 @@ jobs:
|
||||
url="${{ github.server_url }}/$REPO/actions/runs/$run_id"
|
||||
echo "дочерний прогон: $url"
|
||||
echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY"
|
||||
head_sha=$(gh run view "$run_id" --repo "$REPO" --json headSha --jq .headSha)
|
||||
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
|
||||
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
|
||||
- name: "Дождаться Validate"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN_ID: ${{ steps.validate.outputs.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
run_id="$RUN_ID"
|
||||
# Ждём завершения; красный дочерний прогон — красный ночной.
|
||||
gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30
|
||||
|
||||
# #727 К6: ночное пакетное ревью ship на SHA прогона Validate — при любом его
|
||||
# исходе. Dispatch тонкого `ship-review.yml` (`github.token` порождает прогон,
|
||||
# как у Validate; входы и права тонких файлов уже в main). Job ждёт только
|
||||
# появления прогона, не его конца; не появился — предупреждение, не красная
|
||||
# ночь: цвет ночи — цвет Validate.
|
||||
ship_review:
|
||||
name: "Запустить ночное пакетное ревью ship на SHA прогона Validate"
|
||||
needs: dispatch
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: "Запустить ship-ревью и дождаться появления прогона"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
CANDIDATE: ${{ needs.dispatch.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$CANDIDATE" ]; then
|
||||
echo "::warning::нет SHA прогона Validate — ночное ship-ревью не запущено"
|
||||
echo "- Ship-ревью ночью: не запущено — нет SHA прогона Validate" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
since=$(date -u +%FT%TZ)
|
||||
if ! gh workflow run ship-review.yml --ref dev -f tag=nightly -f candidate="$CANDIDATE" --repo "$REPO"; then
|
||||
echo "::warning::ship-ревью ночью не запущено: dispatch отклонён — ночь не красится"
|
||||
echo "- Ship-ревью ночью: dispatch отклонён" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
run_id=""
|
||||
for _ in $(seq 1 18); do
|
||||
sleep 10
|
||||
run_id=$(gh run list --repo "$REPO" --workflow ship-review.yml --branch dev \
|
||||
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
|
||||
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
|
||||
[ -n "$run_id" ] && break
|
||||
done
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::warning::прогон ship-ревью не появился за 3 минуты — ночь не красится, её цвет — цвет Validate"
|
||||
echo "- Ship-ревью ночью (\`$CANDIDATE\`): прогон не появился за 3 минуты" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
echo "- Ship-ревью ночью (\`$CANDIDATE\`): ${{ github.server_url }}/$REPO/actions/runs/$run_id" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# #736: красная ночь — один комментарий в каждую задачу, чьи коммиты классов
|
||||
# A/B вошли в dev после последней зелёной ночи (`scripts/night-red.mjs`,
|
||||
# PROCESS.md §10.4). Только при красном dispatch с известным прогоном;
|
||||
# исход прогона (failure, а не cancelled/timed_out) скрипт читает сам.
|
||||
# Actions — `github.token`, issue — `HP_PROCESS_TOKEN`: `issues: write` у
|
||||
# ночи нет, и потолок тонкого `nightly.yml` в main не меняется. Сбой —
|
||||
# предупреждение, не красная ночь сверх Validate.
|
||||
night_red:
|
||||
name: "Красная ночь: комментарий в задачи, слитые после последней зелёной"
|
||||
needs: dispatch
|
||||
if: always() && needs.dispatch.result == 'failure' && needs.dispatch.outputs.run_id != ''
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
# Нужны коммиты и деревья, не блобы; скрипт и его импорты — без зависимостей, npm ci не нужен.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
filter: blob:none
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: "Комментарий в задачи диапазона от последней зелёной ночи до красной"
|
||||
env:
|
||||
ACTIONS_TOKEN: ${{ github.token }}
|
||||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
RED_RUN: ${{ needs.dispatch.outputs.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Сбои API, токена и git скрипт сам сводит к предупреждению; упавший
|
||||
# скрипт — тоже предупреждение: цвет ночи — цвет Validate.
|
||||
if ! node scripts/night-red.mjs --repo="$REPO" --red-run="$RED_RUN"; then
|
||||
echo "::warning::красная ночь (#736): скрипт упал — комментарии не написаны; цвет ночи — цвет Validate"
|
||||
echo "- Красная ночь (#736): скрипт упал — комментарии не написаны" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
@@ -5,6 +5,12 @@ name: "Метрики процесса · тело (#623)"
|
||||
# идёт в step summary и артефакт; в issue и репозиторий ничего не пишется.
|
||||
# Цифры аудита 22.09 (1,57 раунда код-ревью, S7 ≈ 36 мин/раунд, 226 Validate
|
||||
# за неделю) были собраны руками за час — теперь они стоят один запуск.
|
||||
#
|
||||
# #728: эффект процесса по трекам — отрезки, возвраты с причинами, находки
|
||||
# ship, job-минуты по стадиям и сравнение до/после 28.09. Прогон дороже:
|
||||
# jobs до 600 прогонов и таймлайны задач окна сравнения (≈ 600–800 запросов),
|
||||
# поэтому потолок 30 минут; трек инфраструктуры и объём задачи читаются из
|
||||
# истории git — нужна полная история dev.
|
||||
|
||||
on:
|
||||
# #623: тело вызывается тонким файлом `process-metrics.yml` из ветки по умолчанию
|
||||
@@ -25,15 +31,16 @@ permissions:
|
||||
jobs:
|
||||
metrics:
|
||||
name: "Снимок недели: issue, раунды, прогоны"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# Код — из dev, как у reconcile: расписание читается из main, а исполняется
|
||||
# версия, которую проверил CI.
|
||||
# версия, которую проверил CI. Полная история (#728): коммиты задачи по
|
||||
# трейлеру `Issue: #NN` дают признак инфраструктуры и объём для сравнения.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 1
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
|
||||
@@ -19,7 +19,7 @@ permissions:
|
||||
jobs:
|
||||
reconcile:
|
||||
name: "Один снимок S4/S7 без polling модели"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
concurrency:
|
||||
group: process-reconcile
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
resume:
|
||||
name: "Разбудить раунд, ждавший этот Validate"
|
||||
if: github.event.workflow_run.event == 'workflow_dispatch' && startsWith(github.event.workflow_run.head_branch, 'issue/')
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
concurrency:
|
||||
group: process-resume-${{ github.event.workflow_run.head_branch }}
|
||||
@@ -54,7 +54,12 @@ jobs:
|
||||
SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
EVENT: ${{ github.event.workflow_run.event }}
|
||||
STATUS: ${{ github.event.workflow_run.status }}
|
||||
VALIDATE_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
# #751: без pipefail код конвейера — код tee, и исключение скрипта (gh,
|
||||
# API) проходило зелёным шагом: событие возобновления терялось до
|
||||
# прохода process-reconcile.
|
||||
run: |
|
||||
set -o pipefail
|
||||
node scripts/process-resume.mjs \
|
||||
--repo="$REPO" --branch="$BRANCH" --sha="$SHA" \
|
||||
--event="$EVENT" --status="$STATUS" --apply=true | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
--event="$EVENT" --status="$STATUS" --run-id="$VALIDATE_RUN_ID" --apply=true | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
+494
-134
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
name: "Бета: пакетное ревью ship · тело (#716)"
|
||||
|
||||
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
|
||||
# правка в механических рамках и зелёный лёгкий Validate. Их код читается
|
||||
# пакетно ночью; перед бетой — только непрочитанные или изменённые задачи.
|
||||
# Гейт публикации беты (`ship-review.mjs check`) требует покрытие всех ship-задач
|
||||
# диапазона «прошлый тег..кандидат»; ночного документа с тем же патч-набором
|
||||
# достаточно, отдельный документ тега беты нужен не всегда (#727).
|
||||
#
|
||||
# Три job, как у release-review.yml (#551, #556): детерминированный вход,
|
||||
# модель без права записи, детерминированная публикация документа в `dev`.
|
||||
#
|
||||
# #727: `tag=nightly` — ночной режим. Его запускает `_nightly.yml` после
|
||||
# полного Validate с `candidate` = голова `dev` того прогона. Читаются только
|
||||
# ship-задачи без покрытия или изменившиеся после ревью (`shipCoverage`), документ —
|
||||
# `SHIP-REVIEW-<база>-dev-<sha12>.md`. Бета так же читает только дельту.
|
||||
|
||||
on:
|
||||
# #716: тело вызывается тонким файлом `ship-review.yml` по ссылке `@dev`;
|
||||
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
|
||||
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
|
||||
workflow_call:
|
||||
inputs:
|
||||
# #727: `nightly` — зарезервированное значение (ночной режим, candidate обязателен).
|
||||
tag:
|
||||
description: "Beta tag the review is for, for example v1.79.0-beta.1"
|
||||
required: true
|
||||
type: string
|
||||
candidate:
|
||||
description: "Exact candidate SHA; empty = the dev tip"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
force:
|
||||
description: "Review again even when the document already exists in dev"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Concurrency уровня workflow — у вызывающего `ship-review.yml` (#716).
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: "Ship-ревью: задачи диапазона"
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
outputs:
|
||||
proceed: ${{ steps.range.outputs.proceed }}
|
||||
candidate: ${{ steps.range.outputs.candidate }}
|
||||
base: ${{ steps.range.outputs.base }}
|
||||
doc: ${{ steps.range.outputs.doc }}
|
||||
issues: ${{ steps.range.outputs.issues }}
|
||||
mode: ${{ steps.range.outputs.mode }}
|
||||
patches: ${{ steps.range.outputs.patches }}
|
||||
subject: ${{ steps.range.outputs.subject }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Кандидат, база и ship-задачи
|
||||
id: range
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
CANDIDATE: ${{ inputs.candidate }}
|
||||
FORCE: ${{ inputs.force }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
# Отказ prepare за `| tee` не должен читаться как «ship-задач нет» (#727).
|
||||
set -o pipefail
|
||||
git fetch -q --tags origin
|
||||
# #727: tag=nightly без candidate — отказ до подстановки вершины dev.
|
||||
node scripts/ship-review.mjs mode --tag="$TAG" --candidate="$CANDIDATE"
|
||||
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
|
||||
git cat-file -e "$CANDIDATE^{commit}"
|
||||
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
|
||||
doc=$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")
|
||||
if [ "$FORCE" != "true" ] && git cat-file -e "origin/dev:$doc" 2>/dev/null; then
|
||||
echo "::notice::$doc уже есть в dev — повторное ревью не запускается (force=true, чтобы переснять)"
|
||||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
out="$RUNNER_TEMP/ship-review-input"
|
||||
node scripts/ship-review.mjs prepare --tag="$TAG" --candidate="$CANDIDATE" --force="$FORCE" \
|
||||
--out="$out" --repo="${{ github.repository }}" --run-url="$RUN_URL" | tee "$RUNNER_TEMP/range.txt"
|
||||
cat "$RUNNER_TEMP/range.txt" >> "$GITHUB_OUTPUT"
|
||||
echo "candidate=$CANDIDATE" >> "$GITHUB_OUTPUT"
|
||||
issues=$(sed -n 's/^issues=//p' "$RUNNER_TEMP/range.txt")
|
||||
if [ -z "$issues" ]; then
|
||||
# #727 К5: пустая дельта — все ship-задачи покрыты документами базы.
|
||||
note=$(sed -n 's/^note=//p' "$RUNNER_TEMP/range.txt")
|
||||
echo "::notice::${note:-ship-задач в диапазоне нет — ревью не нужно}"
|
||||
if [ -n "$note" ]; then echo "- $note" >> "$GITHUB_STEP_SUMMARY"; fi
|
||||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||
(cd "$out" && sha256sum brief.md ship-issues.json > manifest.sha256)
|
||||
cat "$out/brief.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Передать вход модели
|
||||
if: steps.range.outputs.proceed == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/ship-review-input
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
model_review:
|
||||
name: "Ship-ревью: работа модели"
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.proceed == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
# #737: строка расхода сессии модели (scripts/model-usage.mjs).
|
||||
outputs:
|
||||
usage: ${{ steps.usage.outputs.line }}
|
||||
# Недоверенная стадия без права записи (#556): документ публикует
|
||||
# `publish`. `github_token` у шага Review обязателен — без него action
|
||||
# меняет OIDC на собственный App-токен с правом записи.
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.prepare.outputs.candidate }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Получить вход
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/ship-review-input
|
||||
|
||||
- name: Проверить вход и кандидата
|
||||
env:
|
||||
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
|
||||
run: |
|
||||
(cd "$RUNNER_TEMP/ship-review-input" && sha256sum -c manifest.sha256)
|
||||
test "$(git rev-parse HEAD)" = "$CANDIDATE"
|
||||
|
||||
- name: Установить зависимости
|
||||
run: npm ci
|
||||
|
||||
- name: Кэш браузеров Playwright
|
||||
id: pw
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Установить Chromium
|
||||
if: steps.pw.outputs.cache-hit != 'true'
|
||||
run: npx playwright install chromium
|
||||
|
||||
# Тот же обход, что у конвейера (_process.yml, anthropics issue 1817).
|
||||
- name: Установить Claude Code детерминированно
|
||||
id: claude_bin
|
||||
run: |
|
||||
src=$(ls "$RUNNER_WORKSPACE"/../_actions/anthropics/claude-code-*/v1/src/entrypoints/run.ts 2>/dev/null | head -1)
|
||||
ver=$(grep -oE 'claudeCodeVersion = "[0-9]+\.[0-9]+\.[0-9]+"' "$src" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
|
||||
ver="${ver:-2.1.265}"
|
||||
base=https://downloads.claude.ai/claude-code-releases
|
||||
bin="$HOME/.local/bin/claude"
|
||||
mkdir -p "$(dirname "$bin")"
|
||||
curl -fsSL --retry 3 "$base/$ver/linux-x64/claude" -o "$bin"
|
||||
sum=$(curl -fsSL --retry 3 "$base/$ver/manifest.json" | jq -r '.platforms["linux-x64"].checksum')
|
||||
echo "$sum $bin" | sha256sum -c -
|
||||
chmod +x "$bin"
|
||||
"$bin" --version
|
||||
echo "path=$bin" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Review
|
||||
id: review
|
||||
uses: anthropics/claude-code-action@9cdae7f0d995e3ba7c33f226087fdf82a59cd520 # v1
|
||||
env:
|
||||
REVIEW_DOC: ${{ runner.temp }}/ship-review.md
|
||||
REVIEW_INPUT: ${{ runner.temp }}/ship-review-input
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
|
||||
prompt: |
|
||||
Ты ревьюер кода проекта House Plan. Язык ответа — русский.
|
||||
|
||||
${{ needs.prepare.outputs.subject }}
|
||||
Кандидат ${{ needs.prepare.outputs.candidate }} (рабочая копия уже на нём) · база
|
||||
диапазона: ${{ needs.prepare.outputs.base || 'нет' }}.
|
||||
Задачи: ${{ needs.prepare.outputs.issues }} — вход в $REVIEW_INPUT/brief.md
|
||||
(строка ТЗ каждой задачи и её коммиты) и $REVIEW_INPUT/ship-issues.json.
|
||||
|
||||
Эти задачи шли по треку ship и слиты в dev без ревью модели: правка
|
||||
в механических рамках и зелёный лёгкий Validate. Ты — первый, кто
|
||||
читает их код. Правила — docs/process/REVIEWER.md, раздел
|
||||
«Пакетное ревью ship», канон — PROCESS.md §5 и §11.7. Прочитай их
|
||||
первыми.
|
||||
|
||||
По каждой задаче: `git show <sha>` каждого коммита; делает ли код
|
||||
то, что сказано в строке ТЗ, и только это; не ломает ли соседнее
|
||||
поведение; не вышла ли правка из трека ship по смыслу (новый
|
||||
UX-контракт, геометрия, конфиг, перф) — это находка Medium с
|
||||
предложением трека. Исполняй, где дёшево: `npm test`, смоук,
|
||||
который называет строка ТЗ или выбирает
|
||||
`node scripts/smoke-select.mjs --base <sha>^ --head <sha>`.
|
||||
|
||||
Серьёзность: High — дефект, который увидит пользователь беты, или
|
||||
правка не делает заявленного; High блокирует публикацию беты.
|
||||
Medium — поведение вне заявленного или вне рамок трека; Low —
|
||||
остальное. Находка чинится отдельной задачей, ты ничего не правишь.
|
||||
|
||||
Ты ничего не правишь и не публикуешь: ни код, ни issue, ни
|
||||
комментарии. Права на запись у тебя нет. Любые изменения рабочей
|
||||
копии будут отброшены — после проверок восстанови её сам
|
||||
(`git checkout -- . && git clean -fd`), если что-то менял.
|
||||
|
||||
Напиши документ в файл по пути из переменной REVIEW_DOC (абсолютный,
|
||||
вне репозитория). Первой строкой после заголовка —
|
||||
`Итог: High N · Medium N · Low N`. Затем по задаче на раздел
|
||||
`## #NN — заголовок`: что проверил (команда → результат), находки с
|
||||
воспроизведением, чего не проверял.
|
||||
|
||||
Затем верни JSON по схеме — последнее обязательное действие.
|
||||
claude_args: |
|
||||
--max-turns 150
|
||||
--allowedTools Read,Write,Grep,Glob,Bash
|
||||
--json-schema '{"type":"object","properties":{"high":{"type":"integer"},"medium":{"type":"integer"},"low":{"type":"integer"},"summary":{"type":"string"}},"required":["high","medium","low","summary"]}'
|
||||
|
||||
# #737: как у конвейера (_process.yml) — расход выходом job, не через
|
||||
# artifact; execution_file не выгружается и не печатается.
|
||||
- name: Снять расход модели
|
||||
id: usage
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
EXEC: ${{ steps.review.outputs.execution_file }}
|
||||
run: |
|
||||
line=$(node scripts/model-usage.mjs --execution-file="$EXEC")
|
||||
echo "line=$line" >> "$GITHUB_OUTPUT"
|
||||
echo "- расход модели: \`$line\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Запечатать результат модели
|
||||
env:
|
||||
SOURCE: ${{ runner.temp }}/ship-review.md
|
||||
OUT: ${{ steps.review.outputs.structured_output }}
|
||||
run: |
|
||||
test -s "$SOURCE" || { echo "::error::модель не оставила документ ревью"; exit 1; }
|
||||
dir="$RUNNER_TEMP/ship-review-result"
|
||||
mkdir -p "$dir"
|
||||
printf '%s' "$OUT" > "$dir/result.json"
|
||||
jq -e '(.high|type=="number") and (.medium|type=="number") and (.low|type=="number") and (.summary|type=="string")' \
|
||||
"$dir/result.json" >/dev/null
|
||||
cp "$SOURCE" "$dir/ship-review.md"
|
||||
(cd "$dir" && sha256sum ship-review.md result.json > manifest.sha256)
|
||||
|
||||
- name: Передать результат публикации
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/ship-review-result
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
publish:
|
||||
name: "Ship-ревью: документ в dev"
|
||||
needs: [prepare, model_review]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Получить результат модели
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/ship-review-result
|
||||
|
||||
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
|
||||
# индекс и коммит решает этот шаг. #727: `mode` и `patches` блока — из
|
||||
# prepare (детерминированный патч-набор), не из результата модели.
|
||||
- name: Опубликовать документ
|
||||
env:
|
||||
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
DOC: ${{ needs.prepare.outputs.doc }}
|
||||
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
|
||||
BASE: ${{ needs.prepare.outputs.base }}
|
||||
ISSUES: ${{ needs.prepare.outputs.issues }}
|
||||
MODE: ${{ needs.prepare.outputs.mode }}
|
||||
PATCHES: ${{ needs.prepare.outputs.patches }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
# #737: расход модели — строкой сразу после блока; недоверенный ввод.
|
||||
USAGE: ${{ needs.model_review.outputs.usage }}
|
||||
run: |
|
||||
dir="$RUNNER_TEMP/ship-review-result"
|
||||
(cd "$dir" && sha256sum -c manifest.sha256)
|
||||
test "$DOC" = "$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")"
|
||||
high=$(jq -r '.high' "$dir/result.json")
|
||||
medium=$(jq -r '.medium' "$dir/result.json")
|
||||
low=$(jq -r '.low' "$dir/result.json")
|
||||
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#730, как #723).
|
||||
# #748: ночью — своё: имя документа без префикса, задача ночного режима.
|
||||
if [ "$MODE" = "nightly" ]; then
|
||||
night=${DOC##*/SHIP-REVIEW-}
|
||||
subject="docs: nightly ship review ${night%.md}"
|
||||
lead="Ночное пакетное ревью задач track:ship (PROCESS.md §11.7)."
|
||||
issue="#727"
|
||||
else
|
||||
subject="docs: ship review for $TAG"
|
||||
lead="Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)."
|
||||
issue="#696"
|
||||
fi
|
||||
msg="$RUNNER_TEMP/ship-review-commit.txt"
|
||||
push_err="$RUNNER_TEMP/ship-review-push.stderr"
|
||||
for attempt in 1 2 3; do
|
||||
git fetch -q origin dev
|
||||
git reset -q --hard origin/dev
|
||||
git clean -fdq
|
||||
mkdir -p docs/reviews
|
||||
{
|
||||
cat "$dir/ship-review.md"
|
||||
printf '\n\n'
|
||||
node -e '
|
||||
import("./scripts/ship-review.mjs").then((m) => process.stdout.write(m.anchorBlock({
|
||||
tag: process.env.TAG, candidate: process.env.CANDIDATE, base: process.env.BASE || null,
|
||||
issues: String(process.env.ISSUES || "").split(",").filter(Boolean).map(Number),
|
||||
high: Number(process.argv[1]), medium: Number(process.argv[2]), low: Number(process.argv[3]),
|
||||
runUrl: process.env.RUN_URL, mode: process.env.MODE, patches: m.parsePatches(process.env.PATCHES),
|
||||
usage: process.env.USAGE ?? "",
|
||||
})));
|
||||
' "$high" "$medium" "$low"
|
||||
} > "$DOC"
|
||||
node scripts/reviews-index.mjs --dir=docs/reviews --strict
|
||||
git add -- "$DOC" docs/reviews/INDEX.md
|
||||
git diff --cached --name-only | node scripts/review-doc-guard.mjs
|
||||
{
|
||||
echo "$subject"
|
||||
echo ""
|
||||
echo "$lead"
|
||||
echo "Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low."
|
||||
echo ""
|
||||
echo "Issue: $issue"
|
||||
echo "User-Visible: no"
|
||||
} > "$msg"
|
||||
git -c user.name="claude[bot]" \
|
||||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||||
commit -q -F "$msg"
|
||||
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
|
||||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
if [ "$MODE" = "nightly" ]; then
|
||||
echo "### Ночное пакетное ревью ship" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "### Предрелизное пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "Задачи $ISSUES · High $high · Medium $medium · Low $low — \`$DOC\` в dev." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "::notice::$DOC опубликован: High $high · Medium $medium · Low $low"
|
||||
exit 0
|
||||
fi
|
||||
# #730: сдвигом dev считается только устаревший lease — отказ
|
||||
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705),
|
||||
# как у ревью релиза (#723). Скрипт — из dev: рабочая копия только что
|
||||
# сброшена на origin/dev. Отказ GitHub (право на workflow, правило
|
||||
# ветки, хук) повтор не лечит: шаг останавливается, причина и ответ
|
||||
# git без токена — в журнале и в сводке шага.
|
||||
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
|
||||
--stage=ship-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||||
if [ "$kind" != "stale" ]; then
|
||||
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
echo "::error::документ ревью не опубликован в dev за три попытки"
|
||||
exit 1
|
||||
|
||||
# #727 К8: High ночного документа — одна строка в каждую его задачу.
|
||||
# Повтор на тот же документ не пишется (метка hp:ship-review-high doc=…),
|
||||
# Medium и Low в issue не идут — их решает владелец (§11.7). Режим и
|
||||
# High проверяет и скрипт: шаг вне ночи или без High ничего не пишет.
|
||||
- name: High ночью — строка в задачи документа
|
||||
if: needs.prepare.outputs.mode == 'nightly'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||||
MODE: ${{ needs.prepare.outputs.mode }}
|
||||
DOC: ${{ needs.prepare.outputs.doc }}
|
||||
ISSUES: ${{ needs.prepare.outputs.issues }}
|
||||
run: |
|
||||
high=$(jq -r '.high' "$RUNNER_TEMP/ship-review-result/result.json")
|
||||
node scripts/ship-review.mjs comment-high --mode="$MODE" --high="$high" --doc="$DOC" \
|
||||
--issues="$ISSUES" --repo="${{ github.repository }}"
|
||||
@@ -43,7 +43,8 @@ jobs:
|
||||
telegram:
|
||||
name: Оповещение в Telegram (только стабильные)
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_call' && inputs.prerelease == false) }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out release notes for a reusable call
|
||||
if: ${{ inputs.reusable == true }}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
name: "Бета: производные артефакты на dev"
|
||||
run-name: "Derived artifacts ${{ inputs.tag }}"
|
||||
|
||||
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
|
||||
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
|
||||
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
|
||||
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
|
||||
# run-name, права и concurrency. Тело — `_beta-derived.yml` по ссылке `@dev`:
|
||||
# правка съёмки и приёмки — один коммит в `dev`, а запуск с любой ветки (в
|
||||
# кнопке по умолчанию выбрана `main`) исполняет тело из `dev`.
|
||||
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
|
||||
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
|
||||
# равными.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
|
||||
required: true
|
||||
type: string
|
||||
docs_expect_change:
|
||||
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
golden_run:
|
||||
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
golden_expect_change:
|
||||
description: "Golden scenes expected to change, comma-separated"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
golden_expect_new:
|
||||
description: "New golden scenes, comma-separated"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: beta-derived
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Потолок прав тела: объединение job-level прав `_beta-derived.yml`. Вызываемый
|
||||
# workflow может права только сузить, поэтому каждая его job по-прежнему
|
||||
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
|
||||
dev:
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
uses: Matysh/houseplan-card/.github/workflows/_beta-derived.yml@dev # #716: тело съёмки и приёмки из dev
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
docs_expect_change: ${{ inputs.docs_expect_change }}
|
||||
golden_run: ${{ inputs.golden_run }}
|
||||
golden_expect_change: ${{ inputs.golden_expect_change }}
|
||||
golden_expect_new: ${{ inputs.golden_expect_new }}
|
||||
secrets: inherit
|
||||
@@ -31,7 +31,8 @@ permissions:
|
||||
jobs:
|
||||
capture:
|
||||
name: Съёмка и сверка скриншот-индекса
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
|
||||
@@ -16,9 +16,10 @@ on:
|
||||
required: false
|
||||
default: dev
|
||||
schedule:
|
||||
# Каждую ночь, 01:00 UTC (04:00 MSK) — после суток правок и до ночного
|
||||
# полного Validate (nightly.yml, 02:30 UTC), чтобы не делить раннеры (#513).
|
||||
- cron: '0 1 * * *'
|
||||
# Каждую ночь, 00:43 UTC (03:43 MSK) — после суток правок и до ночного
|
||||
# полного Validate (nightly.yml, 02:17 UTC), чтобы не делить раннеры (#513).
|
||||
# Минута не круглая: такие старты GitHub сдвигает меньше (#658).
|
||||
- cron: '43 0 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -9,8 +9,10 @@ name: Ночной полный прогон dev
|
||||
# держит копии равными.
|
||||
|
||||
on:
|
||||
# 02:17 UTC, не на круглой минуте: старты «ровно в час/полчаса» GitHub
|
||||
# откладывает на часы (факт — 07:42–08:05 при плане 02:30, #658).
|
||||
schedule:
|
||||
- cron: '30 2 * * *'
|
||||
- cron: '17 2 * * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -10,7 +10,8 @@ on:
|
||||
- ".github/workflows/**"
|
||||
- "docs/**"
|
||||
schedule:
|
||||
- cron: "0 4 * * 1"
|
||||
# Минута не круглая (#658): доходит до main вместе со стабильным релизом.
|
||||
- cron: "11 4 * * 1"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
comparison_ref:
|
||||
@@ -31,7 +32,7 @@ jobs:
|
||||
# Every profile keeps base and candidate sequential on one hosted runner.
|
||||
# Independent profile pairs run in parallel: cross-profile timing is never
|
||||
# compared, while serialising all profile pairs cannot fit the job timeout.
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -39,6 +40,7 @@ jobs:
|
||||
profile:
|
||||
- large-house
|
||||
- isometric
|
||||
- isometric-backdrop
|
||||
- isometric-stage3
|
||||
- plan-snap
|
||||
- interaction
|
||||
@@ -46,6 +48,7 @@ jobs:
|
||||
- overlay
|
||||
- space-default
|
||||
- space-glow
|
||||
- led-strips
|
||||
steps:
|
||||
- name: Check out candidate
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -133,6 +136,10 @@ jobs:
|
||||
npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json
|
||||
npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json
|
||||
;;
|
||||
isometric-backdrop)
|
||||
npm run benchmark:large-house-isometric-backdrop -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-baseline.json
|
||||
npm run benchmark:large-house-isometric-backdrop -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-candidate.json
|
||||
;;
|
||||
isometric-stage3)
|
||||
npm run benchmark:isometric-stage3-dense -- --allow-stage2-base --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-baseline.json
|
||||
npm run benchmark:isometric-stage3-dense -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-candidate.json
|
||||
@@ -169,6 +176,14 @@ jobs:
|
||||
cp ../artifacts/performance/space-glow-candidate.json ../artifacts/performance/space-glow-baseline.json
|
||||
fi
|
||||
;;
|
||||
led-strips)
|
||||
# #780 led-strips-v1: base predates LED strips, so the profile is
|
||||
# judged by its absolute limits inside the runner; the zero-LED
|
||||
# View stays covered by the relative interaction profile above.
|
||||
npm run benchmark:led-strips -- --size=10x5 --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-10x5.json
|
||||
npm run benchmark:led-strips -- --size=50x50 --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-50x50.json
|
||||
npm run benchmark:led-strips -- --size=none --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-none.json
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unknown performance profile: $PROFILE"
|
||||
exit 1
|
||||
@@ -187,6 +202,9 @@ jobs:
|
||||
isometric)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-comparison.json
|
||||
;;
|
||||
isometric-backdrop)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric-backdrop.json --baseline=../artifacts/performance/isometric-backdrop-baseline.json --candidate=../artifacts/performance/isometric-backdrop-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-backdrop-comparison.json
|
||||
;;
|
||||
isometric-stage3)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-isometric-stage3-dense.json --baseline=../artifacts/performance/isometric-stage3-baseline.json --candidate=../artifacts/performance/isometric-stage3-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-stage3-comparison.json
|
||||
;;
|
||||
@@ -208,6 +226,9 @@ jobs:
|
||||
space-glow)
|
||||
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-space-card-glow.json --baseline=../artifacts/performance/space-glow-baseline.json --candidate=../artifacts/performance/space-glow-candidate.json --output=../artifacts/performance/space-glow-comparison.json
|
||||
;;
|
||||
led-strips)
|
||||
echo "led-strips-v1: absolute limits and counters already enforced by the runner (budgets-led-strips.json)"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload full performance report
|
||||
|
||||
@@ -10,8 +10,9 @@ name: Метрики процесса
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Понедельник 05:00 UTC — после ночных прогонов, до рабочего дня.
|
||||
- cron: '0 5 * * 1'
|
||||
# Понедельник 05:23 UTC — после ночных прогонов, до рабочего дня; минута
|
||||
# не круглая, чтобы не стоять в очереди «ровных» расписаний (#658).
|
||||
- cron: '23 5 * * 1'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days:
|
||||
|
||||
@@ -21,7 +21,9 @@ concurrency:
|
||||
jobs:
|
||||
gate:
|
||||
name: "Гейт: зелёная Проверка и релизный контракт"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
# Больше ожидания зелёного Validate в release-gate.mjs (до 60 мин) (#658).
|
||||
timeout-minutes: 75
|
||||
outputs:
|
||||
sha: ${{ steps.candidate.outputs.sha }}
|
||||
tag: ${{ steps.candidate.outputs.tag }}
|
||||
@@ -89,6 +91,14 @@ jobs:
|
||||
REPO: ${{ github.repository }}
|
||||
SHA: ${{ steps.candidate.outputs.sha }}
|
||||
run: node scripts/release-gate.mjs "$SHA"
|
||||
# #696, PROCESS.md §11.7: задачи track:ship слиты без ревью модели — бета
|
||||
# публикуется, только когда их код прочитало пакетное ревью диапазона.
|
||||
- name: Ship tasks of the range are batch-reviewed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ steps.candidate.outputs.tag }}
|
||||
SHA: ${{ steps.candidate.outputs.sha }}
|
||||
run: node scripts/ship-review.mjs check --tag="$TAG" --candidate="$SHA" --repo="$GITHUB_REPOSITORY"
|
||||
- name: Bind issue membership to the exact candidate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -122,7 +132,8 @@ jobs:
|
||||
publish:
|
||||
name: Публикация тега и релиза
|
||||
needs: gate
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
url: ${{ steps.verify.outputs.url }}
|
||||
newly_published: ${{ steps.release.outputs.newly_published }}
|
||||
@@ -289,7 +300,8 @@ jobs:
|
||||
close-merged:
|
||||
name: Закрытие вошедших issue
|
||||
needs: [gate, publish]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
@@ -46,7 +46,7 @@ concurrency:
|
||||
jobs:
|
||||
prepare:
|
||||
name: "Ревью релиза: вход линии"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -75,6 +75,9 @@ jobs:
|
||||
FORCE: ${{ inputs.force }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
# Отказ prepare за `| tee` не должен идти дальше с неполным
|
||||
# GITHUB_OUTPUT и proceed=true (#751).
|
||||
set -o pipefail
|
||||
doc=$(node scripts/release-review.mjs doc --tag="$TAG")
|
||||
git fetch -q --tags origin
|
||||
if [ -z "$CANDIDATE" ]; then
|
||||
@@ -110,7 +113,7 @@ jobs:
|
||||
name: "Ревью релиза: работа модели"
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.proceed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
# Недоверенная стадия. Прав на запись нет никаких: ни в репозиторий, ни в
|
||||
# issue. Документ публикует `publish`; находки в issue превращает владелец.
|
||||
@@ -183,6 +186,14 @@ jobs:
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# #704: `release.yml` ставит ревью в очередь токеном GITHUB_TOKEN, и
|
||||
# прогон начинает `github-actions[bot]` (тип Bot). Action отказывает
|
||||
# ботам, которых нет в списке («Workflow initiated by non-human actor:
|
||||
# github-actions (type: Bot)», v1.78.0). Разрешён ровно этот бот —
|
||||
# не '*': любой другой бот по-прежнему отклоняется. Action сравнивает
|
||||
# имена без регистра и без суффикса `[bot]`. Человек, запустивший
|
||||
# ревью руками, списка не касается.
|
||||
allowed_bots: "github-actions[bot]"
|
||||
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
|
||||
prompt: |
|
||||
Ты независимый ревьюер релиза House Plan. Язык ответа — русский.
|
||||
@@ -207,7 +218,7 @@ jobs:
|
||||
(стенд `demo/`, смоки `demo/smoke_*.mjs`), вводи текст
|
||||
посимвольно, закрывай диалоги настоящим Escape и крестиком. Где
|
||||
есть диалоги HA — пиннутая фикстура `ha-dialog` (#505,
|
||||
demo/helpers/README-ha-dialog-505.md,
|
||||
demo/helpers/README-ha-dialog.md,
|
||||
`node demo/verify_ha_dialog_discard_recovery.mjs`).
|
||||
- Для каждой поверхности: обычный сценарий и самый рискованный
|
||||
соседний, шесть классов риска (async, данные/права, геометрия,
|
||||
@@ -258,7 +269,7 @@ jobs:
|
||||
publish:
|
||||
name: "Ревью релиза: документ в dev"
|
||||
needs: [prepare, model_review]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -295,6 +306,9 @@ jobs:
|
||||
(cd "$dir" && sha256sum -c manifest.sha256)
|
||||
test "$DOC" = "$(node scripts/release-review.mjs doc --tag="$TAG")"
|
||||
counts=$(jq -r '"High \(.high) · Medium \(.medium) · Low \(.low)"' "$dir/result.json")
|
||||
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#723).
|
||||
msg="$RUNNER_TEMP/release-review-commit.txt"
|
||||
push_err="$RUNNER_TEMP/release-review-push.stderr"
|
||||
for attempt in 1 2 3; do
|
||||
git fetch -q origin dev
|
||||
git reset -q --hard origin/dev
|
||||
@@ -310,24 +324,36 @@ jobs:
|
||||
node scripts/reviews-index.mjs --dir=docs/reviews --strict
|
||||
git add -- "$DOC" docs/reviews/INDEX.md
|
||||
git diff --cached --name-only | node scripts/review-doc-guard.mjs
|
||||
{
|
||||
echo "docs: release review for $TAG"
|
||||
echo ""
|
||||
echo "Независимое ревью линии перед стабильным релизом (PROCESS.md §11.5)."
|
||||
echo "Итог: $counts. Выпуск не блокирует; решение по находкам — за владельцем."
|
||||
echo ""
|
||||
echo "Issue: #638"
|
||||
echo "User-Visible: no"
|
||||
} > "$msg"
|
||||
git -c user.name="claude[bot]" \
|
||||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||||
commit -q -F - <<MSG
|
||||
docs: release review for $TAG
|
||||
|
||||
Независимое ревью линии перед стабильным релизом (PROCESS.md §11.5).
|
||||
Итог: $counts. Выпуск не блокирует; решение по находкам — за владельцем.
|
||||
|
||||
Issue: #638
|
||||
User-Visible: no
|
||||
MSG
|
||||
commit -q -F "$msg"
|
||||
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
|
||||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev; then
|
||||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
echo "### Независимое ревью $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Итог: $counts — \`$DOC\` в dev. Выпуск не блокируется." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "::notice::$DOC опубликован: $counts"
|
||||
exit 0
|
||||
fi
|
||||
# #723: сдвигом dev считается только устаревший lease — отказ
|
||||
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705).
|
||||
# Отказ GitHub (право на workflow, правило ветки, хук) повтор не
|
||||
# лечит: шаг останавливается, причина и ответ git без токена — в
|
||||
# журнале и в сводке шага.
|
||||
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
|
||||
--stage=release-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||||
if [ "$kind" != "stale" ]; then
|
||||
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
|
||||
@@ -48,7 +48,8 @@ jobs:
|
||||
name: "Кандидат: точный SHA, режим и черновик"
|
||||
# Публикация беты руками — не наш случай: у бет свой staged-путь.
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.prerelease }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
sha: ${{ steps.resolve.outputs.sha }}
|
||||
tag: ${{ steps.resolve.outputs.tag }}
|
||||
@@ -142,7 +143,7 @@ jobs:
|
||||
needs: candidate
|
||||
if: ${{ needs.candidate.outputs.prerelease != 'true' }}
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -152,19 +153,68 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ needs.candidate.outputs.tag }}
|
||||
SHA: ${{ needs.candidate.outputs.sha }}
|
||||
# #704: сколько ждать, что dispatch стал прогоном, — выпуск не ждёт
|
||||
# дольше нескольких минут; укладывается в timeout-minutes job.
|
||||
APPEAR_SECONDS: 180
|
||||
POLL_SECONDS: 15
|
||||
run: |
|
||||
if gh workflow run release-review.yml --repo "${{ github.repository }}" --ref dev \
|
||||
repo="${{ github.repository }}"
|
||||
# Отсчёт окна — до dispatch, с запасом минута на расхождение часов.
|
||||
since=$(( $(date -u +%s) - 60 ))
|
||||
if ! gh workflow run release-review.yml --repo "${{ github.repository }}" --ref dev \
|
||||
-f tag="$TAG" -f candidate="$SHA"; then
|
||||
echo "Независимое ревью $TAG поставлено в очередь: release-review.yml на dev" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "::warning::ревью линии $TAG не запущено — выпуск продолжается; запустить руками: gh workflow run release-review.yml --ref dev -f tag=$TAG -f candidate=$SHA"
|
||||
echo "Независимое ревью $TAG: **не запущено** — dispatch release-review.yml отклонён." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
# #704: dispatch не возвращает прогона, и v1.78.0 показал цену: ревью
|
||||
# упало в прогоне, о котором выпуск не знал. Прогон ищется по workflow,
|
||||
# ветке dev, событию, времени и run-name «Release review <тег>».
|
||||
# Найден и стартовал — ссылка и статус в сводке; нет — предупреждение,
|
||||
# выпуск не блокируется.
|
||||
title="Release review $TAG"
|
||||
run=""
|
||||
started=false
|
||||
tries=$(( APPEAR_SECONDS / POLL_SECONDS ))
|
||||
for try in $(seq 1 "$tries"); do
|
||||
found=$(gh run list --repo "$repo" --workflow release-review.yml --branch dev \
|
||||
--event workflow_dispatch --limit 20 \
|
||||
--json databaseId,url,status,conclusion,displayTitle,createdAt \
|
||||
| jq -c --arg title "$title" --argjson since "$since" \
|
||||
'[.[] | select(.displayTitle == $title and (.createdAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= $since)] | sort_by(.createdAt) | last // empty') || found=""
|
||||
if [ -n "$found" ]; then
|
||||
run="$found"
|
||||
case "$(jq -r .status <<< "$run")" in
|
||||
in_progress|completed) started=true; break ;;
|
||||
esac
|
||||
fi
|
||||
if [ "$try" -lt "$tries" ]; then sleep "$POLL_SECONDS"; fi
|
||||
done
|
||||
minutes=$(( (APPEAR_SECONDS + 59) / 60 ))
|
||||
if [ -z "$run" ]; then
|
||||
echo "::warning::прогон ревью линии $TAG не появился за $minutes мин — выпуск продолжается; проверить Actions → release-review.yml"
|
||||
echo "Независимое ревью $TAG: **не стартовало за $minutes мин** — прогон release-review.yml на dev не найден." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
url=$(jq -r .url <<< "$run")
|
||||
status=$(jq -r '.status + (if (.conclusion // "") != "" then " · " + .conclusion else "" end)' <<< "$run")
|
||||
if [ "$started" != "true" ]; then
|
||||
echo "::warning::ревью линии $TAG в очереди и не стартовало за $minutes мин: $url"
|
||||
echo "Независимое ревью $TAG: **не стартовало за $minutes мин** (в очереди) — [прогон]($url), статус $status." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$(jq -r '.conclusion // ""' <<< "$run")" = "failure" ]; then
|
||||
echo "::warning::ревью линии $TAG уже упало: $url"
|
||||
fi
|
||||
echo "Независимое ревью $TAG **запущено**: [прогон]($url), статус $status. Выпуск его не ждёт." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
gate:
|
||||
name: "Гейт: контракт, Validate, Full Performance и E2E на точном SHA"
|
||||
needs: candidate
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
# Больше суммы собственных ожиданий job: Validate и Full Performance ждутся
|
||||
# до 60 мин каждый (release-gate.mjs), E2E — до 45 (e2e-gate.mjs): 165 < 180 (#658).
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -234,7 +284,8 @@ jobs:
|
||||
stage:
|
||||
name: "Сборка: ассеты, SHA256SUMS и загрузка в черновик"
|
||||
needs: [candidate, gate]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
@@ -374,7 +425,8 @@ jobs:
|
||||
publish:
|
||||
name: "Публикация и сверка публичных байтов"
|
||||
needs: [candidate, gate, stage]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
url: ${{ steps.verify.outputs.url }}
|
||||
name: ${{ steps.verify.outputs.name }}
|
||||
@@ -474,7 +526,8 @@ jobs:
|
||||
# make that distribution failure impossible to miss in the release run.
|
||||
if: ${{ needs.candidate.outputs.prerelease == 'true' }}
|
||||
needs: [candidate, publish]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Verify the published tag is the prerelease HACS will discover
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: "Бета: пакетное ревью ship"
|
||||
run-name: "Ship review ${{ inputs.tag }}"
|
||||
|
||||
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
|
||||
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
|
||||
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
|
||||
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
|
||||
# run-name, права и concurrency. Тело — `_ship-review.yml` по ссылке `@dev`:
|
||||
# правка ревью — один коммит в `dev`, а запуск с любой ветки (в кнопке по
|
||||
# умолчанию выбрана `main`) исполняет тело из `dev`.
|
||||
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
|
||||
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
|
||||
# равными.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Beta tag the review is for, for example v1.79.0-beta.1"
|
||||
required: true
|
||||
type: string
|
||||
candidate:
|
||||
description: "Exact candidate SHA; empty = the dev tip"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
force:
|
||||
description: "Review again even when the document already exists in dev"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ship-review-${{ inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Потолок прав тела: объединение job-level прав `_ship-review.yml`. Вызываемый
|
||||
# workflow может права только сузить, поэтому каждая его job по-прежнему
|
||||
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
|
||||
dev:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
uses: Matysh/houseplan-card/.github/workflows/_ship-review.yml@dev # #716: тело ревью из dev
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
candidate: ${{ inputs.candidate }}
|
||||
force: ${{ inputs.force }}
|
||||
secrets: inherit
|
||||
+142
-39
@@ -21,12 +21,11 @@ on:
|
||||
description: 'Полный набор: смоки, golden, performance_smoke (без мутантов — #601)'
|
||||
type: boolean
|
||||
default: true
|
||||
# Мутанты по диффу бегут только по явному запросу (#510, #601):
|
||||
# ревью-конвейер и слияние кандидата запускают `-f full=false
|
||||
# -f mutants=true` на материале. `full=true`, трейлер `Release:` и ночь
|
||||
# их не включают — ночью идёт полный реестр (mutation-gate.yml).
|
||||
# #709: мутанты в разработке не гоняются — вход оставлен, чтобы старые
|
||||
# вызовы `-f mutants=…` не падали, но job мутантов по диффу он больше не
|
||||
# включает (classify-changes.mjs). Весь реестр — ночью (mutation-gate.yml).
|
||||
mutants:
|
||||
description: 'Мутанты по диффу на этом SHA (кандидат ревью или слияния)'
|
||||
description: 'Не действует с #709: мутанты — только ночной полный прогон'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -50,14 +49,16 @@ jobs:
|
||||
# действовать — на уровне шагов, с той же гранулярностью в логе.
|
||||
preflight:
|
||||
name: "Предполёт: документация, провенанс, процесс"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
# `actions: read` — чтение списка прогонов Validate (#388), `issues: read` —
|
||||
# проверка 8 процессного гейта. Права перечислены явно, потому что job
|
||||
# обращается к API сверх содержимого репозитория.
|
||||
# проверка 8 процессного гейта, `issues: write` — одно issue о расхождении
|
||||
# зеркала workflow на push в dev (#700). Права перечислены явно, потому что
|
||||
# job обращается к API сверх содержимого репозитория.
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
issues: read
|
||||
issues: write
|
||||
steps:
|
||||
# `blob:none` при полной истории (#345): этой job нужны сообщения
|
||||
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
|
||||
@@ -67,8 +68,10 @@ jobs:
|
||||
# Коммиты и деревья скачиваются целиком, поэтому диапазоны и `merge-base`
|
||||
# работают как раньше. Единственная догрузка по требованию здесь —
|
||||
# `git show origin/main:.github/workflows/<тонкий файл>`: по блобу на файл.
|
||||
# `fetch-tags: true` (#703): теги релиза — граница проверенного материала
|
||||
# для базы диапазона, а checkout по умолчанию качает с `--no-tags`.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with: { fetch-depth: 0, filter: 'blob:none' }
|
||||
with: { fetch-depth: 0, filter: 'blob:none', fetch-tags: true }
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with: { node-version: 22 }
|
||||
|
||||
@@ -80,17 +83,24 @@ jobs:
|
||||
id: docs
|
||||
continue-on-error: true
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
|
||||
FULL_INPUT: ${{ inputs.full }}
|
||||
# #697: на ветке задачи `Release:` строгий режим не включает.
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
# #700: на ветке задачи упавший чужой сайт — предупреждение, а не
|
||||
# красный preflight; push в dev, кандидат и релиз судят ссылки строго.
|
||||
external=--external
|
||||
case "$REF" in refs/heads/issue/*) external=--external=warn ;; esac
|
||||
# #586: CLI отдаёт ОДИН ответ. Прежде здесь сравнивался со строкой
|
||||
# `heavy=true` весь вывод `--heavy`, а он двухстрочный: в `$(…)`
|
||||
# строки схлопываются через пробел, сравнение не совпадало никогда,
|
||||
# и строгий режим не включился ни на одном кандидате.
|
||||
mode=$(node scripts/classify-changes.mjs --screenshots-mode)
|
||||
echo "скриншоты документации: режим $mode"
|
||||
node scripts/check-docs.mjs --external --screenshots=$mode
|
||||
node scripts/check-docs.mjs "$external" --screenshots=$mode
|
||||
|
||||
# #635: `docs/reviews/INDEX.md` — снимок каталога ревью; расхождение с
|
||||
# каталогом — невидимые через индекс документы (r2 #635 H1). Гейт стоит
|
||||
@@ -114,9 +124,16 @@ jobs:
|
||||
# ни одним событием. Тонкий файл сверяется: правка триггера или потолка
|
||||
# прав, не доехавшая до `main`, действовала бы только в dev-копии.
|
||||
#
|
||||
# #716: `ship-review.yml` и `beta-derived.yml` — только `workflow_dispatch`,
|
||||
# он исполняет файл с выбранной ветки, но запуск GitHub даёт лишь workflow,
|
||||
# чей файл есть в `main`. Шагам беты он нужен до промоушена, поэтому они
|
||||
# устроены так же и сверяются здесь же: правка входа или потолка прав,
|
||||
# не доехавшая до `main`, не действовала бы в запуске с `main`, а именно
|
||||
# его кнопка выбирает по умолчанию.
|
||||
#
|
||||
# `performance.yml` не входит: по расписанию он судит `main` собственным
|
||||
# телом из `main`, расхождение с `dev` до промоушена законно. Список ниже
|
||||
# держит равным множеству файлов с такими триггерами
|
||||
# держит равным множеству тонких файлов
|
||||
# test/default-branch-workflows.test.mjs.
|
||||
- name: "Процесс: тонкие вызывающие workflow идентичны в main и dev"
|
||||
id: workflow_sync
|
||||
@@ -124,7 +141,7 @@ jobs:
|
||||
run: |
|
||||
git fetch --quiet origin main dev
|
||||
status=0
|
||||
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml; do
|
||||
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml ship-review.yml beta-derived.yml; do
|
||||
if diff <(git show "origin/main:.github/workflows/$file") \
|
||||
<(git show "origin/dev:.github/workflows/$file"); then
|
||||
echo "$file: main и dev идентичны"
|
||||
@@ -137,6 +154,39 @@ jobs:
|
||||
done
|
||||
exit $status
|
||||
|
||||
# #700: расхождение зеркала на dev — одно открытое issue владельцу, как у
|
||||
# ночного мутационного гейта (#472): чинит его тот, кто зеркалит в main, а
|
||||
# не автор задачи, чья ветка ни при чём. Сбой API — предупреждение: шаг
|
||||
# сообщает, а не судит.
|
||||
- name: "Расхождение зеркала на dev — issue владельцу"
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && steps.workflow_sync.outcome == 'failure'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
marker="[workflow-sync]"
|
||||
# Список не прочитан — нового issue нет: иначе сбой сети или
|
||||
# рейт-лимит плодил бы дубликат к уже открытому (r1 #700).
|
||||
if ! existing=$(gh issue list --repo "$REPO" --state open --search "\"$marker\" in:title" \
|
||||
--json number,title --jq '[.[] | select(.title | startswith("[workflow-sync]"))][0].number // empty'); then
|
||||
echo "::warning::открытые issue не прочитаны — новое не заводится, чтобы не завести дубликат"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$existing" ]; then
|
||||
gh issue comment "$existing" --repo "$REPO" --body "Расхождение повторилось: $RUN_URL" \
|
||||
|| echo "::warning::комментарий в #$existing не оставлен"
|
||||
exit 0
|
||||
fi
|
||||
cat > /tmp/workflow-sync.md <<EOF
|
||||
Тонкие вызывающие workflow в \`main\` и \`dev\` различаются. Прогон: $RUN_URL
|
||||
|
||||
Тонкий файл исполняется из ветки по умолчанию, поэтому правку триггеров, входов или прав нужно зеркалить в \`main\` (PROCESS.md §10.4, #623). На ветках задач это предупреждение (#700), на push в \`dev\` — красный preflight, пока зеркало не выровнено.
|
||||
EOF
|
||||
gh issue create --repo "$REPO" --title "$marker тонкие workflow в main и dev различаются" \
|
||||
--label infra --label process --body-file /tmp/workflow-sync.md \
|
||||
|| echo "::warning::issue о расхождении зеркала не заведено"
|
||||
|
||||
# Оба гейта ниже судят САМ диапазон коммитов, а не объём проверок, и до
|
||||
# #388 брали его от головы предыдущего пуша. Прогон предыдущего пуша
|
||||
# штатно отменяется следующим (concurrency), и тогда его коммиты не судит
|
||||
@@ -146,9 +196,14 @@ jobs:
|
||||
# прежним `before`: расширять диапазон здесь нельзя, иначе гейт, который
|
||||
# сам красит прогон, лишает следующий пуш зелёного предка и запирает dev
|
||||
# в красноте навсегда.
|
||||
#
|
||||
# #703: то же на `main`. Stable-промоушен — пуш в `main` SHA, уже
|
||||
# судимого на `dev`; с `event.before` (прошлый stable) провенанс и
|
||||
# процессный гейт судили бы всю бета-линию заново. Прогоны читаются с
|
||||
# обеих интеграционных веток, сам HEAD засчитывает только успешный.
|
||||
- name: "База диапазона: последний доказанно зелёный предок"
|
||||
id: range
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
@@ -158,11 +213,14 @@ jobs:
|
||||
# `status=completed`, а не `success`: гейтам диапазона нужен факт
|
||||
# «коммит судили», а не «вердикт был оправдательный». Упавший прогон
|
||||
# коммит судил; отменённый — нет, его отсеивает judgedShas (#388).
|
||||
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
|
||||
-f branch=dev -f status=completed -F per_page=100 \
|
||||
> /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json
|
||||
for branch in dev main; do
|
||||
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
|
||||
-f branch="$branch" -f status=completed -F per_page=100 \
|
||||
> "/tmp/validate-runs-$branch.json" || echo '{}' > "/tmp/validate-runs-$branch.json"
|
||||
done
|
||||
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
|
||||
--fallback="$FALLBACK" --runs=/tmp/validate-runs.json
|
||||
--fallback="$FALLBACK" \
|
||||
--runs=/tmp/validate-runs-dev.json --runs=/tmp/validate-runs-main.json
|
||||
- name: "Провенанс коммитов: трейлеры и эталоны"
|
||||
id: provenance
|
||||
continue-on-error: true
|
||||
@@ -217,6 +275,7 @@ jobs:
|
||||
PROCESS_GATE: ${{ steps.process_gate.outcome }}
|
||||
ACTION_PINS: ${{ steps.action_pins.outcome }}
|
||||
REVIEWS_INDEX: ${{ steps.reviews_index.outcome }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
fail=0
|
||||
check() {
|
||||
@@ -228,9 +287,25 @@ jobs:
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
# #700: причина, к которой ветка задачи отношения не имеет, — в сводку
|
||||
# предупреждением; на dev, кандидате и релизе она красит как прежде.
|
||||
advise() {
|
||||
if [ "$2" = "success" ]; then
|
||||
echo "ok $1"
|
||||
else
|
||||
echo "::warning::$1 ($2) — на ветке задачи предупреждение (#700)"
|
||||
echo "- $1: $2 — предупреждение, чинит релиз-менеджер (#700)" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
}
|
||||
task_branch=false
|
||||
case "$REF" in refs/heads/issue/*) task_branch=true ;; esac
|
||||
echo "### Предполётные проверки" >> "$GITHUB_STEP_SUMMARY"
|
||||
check "документация" "$DOCS"
|
||||
check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
|
||||
if [ "$task_branch" = "true" ]; then
|
||||
advise "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
|
||||
else
|
||||
check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
|
||||
fi
|
||||
check "провенанс коммитов" "$PROVENANCE"
|
||||
check "процессный гейт" "$PROCESS_GATE"
|
||||
check "пины сторонних Actions" "$ACTION_PINS"
|
||||
@@ -247,7 +322,8 @@ jobs:
|
||||
# там делает код-ревью, которое гоняет гейты само (#127).
|
||||
changes:
|
||||
name: Классификация изменённых файлов
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
# Чтение списка прогонов Validate — единственное, что этой job нужно сверх
|
||||
# содержимого репозитория (#387).
|
||||
permissions:
|
||||
@@ -271,16 +347,19 @@ jobs:
|
||||
range_base: ${{ steps.base.outputs.range_base }}
|
||||
steps:
|
||||
# `git diff --name-only` содержимого файлов не читает вовсе, поэтому
|
||||
# блобы истории этой job не нужны ни на одном шаге (#345).
|
||||
# блобы истории этой job не нужны ни на одном шаге (#345). Теги — нужны:
|
||||
# они граница проверенного материала для `range_base` (#703), а checkout
|
||||
# по умолчанию передаёт `git fetch --no-tags`.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with: { fetch-depth: 0, filter: 'blob:none' }
|
||||
with: { fetch-depth: 0, filter: 'blob:none', fetch-tags: true }
|
||||
# База диапазона — самый новый предок с УСПЕШНО завершённым Validate
|
||||
# (#387). Прежде бралась голова предыдущего пуша (`github.event.before`),
|
||||
# то есть допущение «до этого всё проверено». Concurrency отменяет прогон
|
||||
# предыдущего пуша штатно, и на #86 (r5) это дало зелёный статус ветки
|
||||
# без единого исполненного тяжёлого гейта.
|
||||
# Смоки, golden, performance_smoke — на кандидате беты (трейлер
|
||||
# `Release:`), по `workflow_dispatch full=true`, на PR (#479). Логика и
|
||||
# `Release:` вне веток задач, #697), по `workflow_dispatch full=true`, на
|
||||
# PR (#479). Логика и
|
||||
# её тест — scripts/classify-changes.mjs.
|
||||
- id: heavy
|
||||
env:
|
||||
@@ -288,7 +367,12 @@ jobs:
|
||||
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
|
||||
FULL_INPUT: ${{ inputs.full }}
|
||||
MUTANTS_INPUT: ${{ inputs.mutants }}
|
||||
run: node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
# #751: без pipefail упавший classify-changes оставлял heavy пустым —
|
||||
# тяжёлые job молча пропускались, а job changes зеленела.
|
||||
run: |
|
||||
set -o pipefail
|
||||
node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
|
||||
- id: base
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
@@ -310,9 +394,15 @@ jobs:
|
||||
if [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; then
|
||||
# На интеграционных ветках классифицировать нечего (всё true), но
|
||||
# база диапазона нужна гейту «новый код не добавляет any» в job
|
||||
# frontend (#388, #619).
|
||||
# frontend (#388, #619). #703: прогоны обеих интеграционных веток —
|
||||
# на `main` приходит SHA, уже судимый на `dev`.
|
||||
other=dev; [ "$BRANCH" = "dev" ] && other=main
|
||||
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
|
||||
-f branch="$other" -f status=completed -F per_page=100 \
|
||||
> /tmp/validate-runs-other.json || echo '{}' > /tmp/validate-runs-other.json
|
||||
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
|
||||
--name=range_base --fallback="$FALLBACK" --runs=/tmp/validate-runs.json
|
||||
--name=range_base --fallback="$FALLBACK" \
|
||||
--runs=/tmp/validate-runs.json --runs=/tmp/validate-runs-other.json
|
||||
exit 0
|
||||
fi
|
||||
merge_base=$(git merge-base origin/dev "$HEAD_SHA" || git rev-parse "$HEAD_SHA~1")
|
||||
@@ -406,7 +496,8 @@ jobs:
|
||||
# performance_smoke включает набор, иначе glow-only результат засчитался бы
|
||||
# прогону, которому нужен изометрический профиль.
|
||||
needs: changes
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
smoke: ${{ steps.probe.outputs.smoke }}
|
||||
golden: ${{ steps.probe.outputs.golden }}
|
||||
@@ -555,7 +646,8 @@ jobs:
|
||||
name: "HACS: валидация репозитория"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.integration == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: HACS validation
|
||||
@@ -567,7 +659,8 @@ jobs:
|
||||
name: "Hassfest: манифест интеграции"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.integration == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Hassfest validation
|
||||
@@ -585,6 +678,10 @@ jobs:
|
||||
# слияние кандидата запускают Validate по кнопке с `mutants=true` на
|
||||
# материале; PR берёт её сам. За 08–09.09 на промежуточных пушах она стоила
|
||||
# 48 из 56 часов job-минут и в основном отменялась следующим пушем. С #601
|
||||
# С #709 job не запрашивает никто: мутанты в разработке не гоняются, весь
|
||||
# реестр проверяет ночь (mutation-gate.yml). Job оставлен до отдельной уборки
|
||||
# вместе с контрактом имён job (#622); `mutants_requested` всегда false.
|
||||
# Ниже — история до #709.
|
||||
# её не запрашивают ни `full=true`, ни кандидат беты (`Release:`): к бете
|
||||
# каждая задача уже прогнана мутантами на ревью и на слитом кандидате, а
|
||||
# ночь покрыта полным реестром. Когда мутанты запрошены, job бежит даже при
|
||||
@@ -598,7 +695,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -727,7 +824,8 @@ jobs:
|
||||
name: "Фронтенд: типы, юниты, мутанты, синхрон бандла"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
# Полная история без блобов (#342): гейт «новый код не добавляет any»
|
||||
# diff-aware, ему нужен диапазон, а содержимое старых ревизий — нет.
|
||||
@@ -837,7 +935,7 @@ jobs:
|
||||
name: "Бандл головы dev для стенда"
|
||||
needs: frontend
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && needs.frontend.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
@@ -867,7 +965,7 @@ jobs:
|
||||
# Gated on `frontend` so a typecheck failure does not burn browser minutes.
|
||||
needs: [changes, frontend, reuse]
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.smoke != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
# Смоки шардируются: последовательный прогон занимал ~7.5 минут и был
|
||||
# критическим путём всего Validate. Три шарда режут его примерно вдвое;
|
||||
@@ -974,7 +1072,8 @@ jobs:
|
||||
name: "Смоки: все шарды зелёные"
|
||||
needs: [changes, smoke, reuse]
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.smoke != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Записать маркер успеха
|
||||
run: |
|
||||
@@ -998,7 +1097,8 @@ jobs:
|
||||
# inexpensive and catches a different class of regressions than timings.
|
||||
needs: [changes, frontend, reuse]
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.golden != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
@@ -1100,7 +1200,7 @@ jobs:
|
||||
# prereleases. The expensive same-runner comparison lives in performance.yml.
|
||||
needs: [changes, frontend, reuse]
|
||||
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.performance_smoke != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
# 15 минут не хватало, когда установка браузера шла через apt: замер
|
||||
# начинался на исходе окна (#206). Запас на холодный кэш — при попадании
|
||||
# job укладывается в те же минуты, что и раньше. Диффозависимые профили
|
||||
@@ -1196,7 +1296,8 @@ jobs:
|
||||
name: "Геометрия: TS/Python parity исполнена"
|
||||
needs: [changes, reuse]
|
||||
if: needs.changes.outputs.geometry_parity == 'true' && needs.reuse.outputs.geometry_parity != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
@@ -1230,7 +1331,8 @@ jobs:
|
||||
name: "Бэкенд: pytest в Home Assistant"
|
||||
needs: [changes, reuse]
|
||||
if: needs.changes.outputs.backend == 'true' && needs.reuse.outputs.backend != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
# Browser fixtures are generated by their real ESM factories and then
|
||||
@@ -1316,7 +1418,8 @@ jobs:
|
||||
name: "Доказательство выполненных проверок"
|
||||
if: always()
|
||||
needs: [preflight, changes, reuse, hacs, hassfest, changed_mutants, frontend, smoke, smoke_done, golden, performance_smoke, geometry_parity, backend]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
|
||||
@@ -4,89 +4,51 @@ House Plan is one HACS package with two parts plus a demo harness:
|
||||
|
||||
- **Lovelace card** (`src/`, TypeScript + Lit) — the primary product, bundled to
|
||||
the entry, manifest and hashed chunks under `dist/`.
|
||||
- **Storage integration** (`custom_components/houseplan/`, Python) — the Home Assistant backend.
|
||||
- **Demo harness** (`demo/`) — a self-contained Playwright page (`demo/srv/demo.html`) that renders the card against a fake `hass`, used for screenshots and the `smoke_*.mjs` end-to-end suite.
|
||||
- **Storage integration** (`custom_components/houseplan/`, Python) — the Home
|
||||
Assistant backend.
|
||||
- **Demo harness** (`demo/`) — a Playwright page (`demo/srv/demo.html`) that
|
||||
renders the card against a fake `hass` for screenshots and the `smoke_*.mjs`
|
||||
suite. The home is fully synthetic. Launcher `demo/serve.mjs`; golden scenes
|
||||
`demo/golden/`, performance `demo/performance/`, guard suite `demo/guard/`,
|
||||
live stand seed `demo/stand/` — each with its own README.
|
||||
|
||||
This file is the map and the few rules every session needs before its first
|
||||
command. `PROCESS.md` is the only complete canon and wins any disagreement;
|
||||
the sections below link to it instead of retelling it.
|
||||
|
||||
## Read this first
|
||||
|
||||
**`docs/SCOPE.md` before anything else.** It was fixed with the owner and states
|
||||
its own authority: features are built, improved and accepted **only** if they
|
||||
serve a job listed there. It carries the mission, the three personas, the core
|
||||
user jobs and the out-of-scope list.
|
||||
|
||||
Its central consequence: **View mode is the product for two of the three
|
||||
personas.** Editors are admin-only tools and must never leak interactions into
|
||||
View.
|
||||
|
||||
For work that changes visible behaviour, also read `docs/USER-GUIDE.ru.md` —
|
||||
interface wording comes from there and is not invented, or the UI starts speaking
|
||||
developer.
|
||||
serve a job listed there. Its central consequence: **View mode is the product
|
||||
for two of the three personas.** Editors are admin-only tools and must never
|
||||
leak interactions into View. For work that changes visible behaviour, also read
|
||||
`docs/USER-GUIDE.ru.md` — interface wording comes from there and is not invented.
|
||||
|
||||
**Reading order by role** (#634). `node scripts/entry-cost.mjs` measures each
|
||||
route and `test/entry-cost.test.mjs` keeps this list equal to its routes:
|
||||
|
||||
- author (analysis, spec, implementation, infrastructure): `docs/SCOPE.md` →
|
||||
`AGENTS.md` → `docs/process/AUTHOR.md` → `docs/STATUS.md`;
|
||||
`AGENTS.md` → `docs/process/AUTHOR.md`, then the task packet
|
||||
(`node scripts/task-packet.mjs --issue NN`); the status snapshot
|
||||
(docs/STATUS.md) only when resuming a session or preparing a release;
|
||||
- reviewer (spec or code): `docs/SCOPE.md` → `AGENTS.md` →
|
||||
`docs/process/REVIEWER.md`, then the issue body and its comments;
|
||||
- changing the pipeline, the gates or the process itself: `docs/SCOPE.md` →
|
||||
`AGENTS.md` → `PROCESS.md` → `docs/STATUS.md`.
|
||||
|
||||
The two digests quote and link `PROCESS.md` section by section; it stays the
|
||||
only complete canon and wins any disagreement, so open the linked section
|
||||
whenever a digest line governs your current step. For non-trivial changes add
|
||||
`docs/ARCHITECTURE.md` plus the canonical document of the subsystem you touch:
|
||||
The two digests quote and link `PROCESS.md` section by section; open the linked
|
||||
section whenever a digest line governs your current step. For non-trivial
|
||||
changes add `docs/ARCHITECTURE.md` plus the canonical document of the subsystem
|
||||
you touch (one list, the same one the reviewer prompt in `_process.yml` reads):
|
||||
`SUN.md`, `LIGHT.md`, `CANVAS.md`, `WALL-THICKNESS.md`, `UX-MODES.md`,
|
||||
`CONFIG-COMPATIBILITY.md`, `TOUCH-SUPPORT.md`.
|
||||
`CONFIG-COMPATIBILITY.md`, `TOUCH-SUPPORT.md`, `ISOMETRIC.md`, `VACUUM.md`,
|
||||
`DECOR-EDITOR.md`, `DEVICE-PRESENTATION.md`, `FILTERING.md`, `STAIRS.md`,
|
||||
`RADAR.md`, `PDF-EXPORT.md`, `STYLING-HOOKS.md`.
|
||||
|
||||
Standard commands live in `package.json` scripts, `CONTRIBUTING.md` and
|
||||
`docs/DEVELOPMENT.md`.
|
||||
|
||||
## Canonical backlog and status
|
||||
|
||||
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical
|
||||
task records: problem, scope, acceptance criteria and discussion.
|
||||
|
||||
**Status lives in labels:** `S1-new`, `S2-analysis`, `S3-spec`, `S4-spec-review`,
|
||||
`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`, plus `blocked` on top
|
||||
of a status and `rejected` on a closed issue. A product issue in flight carries
|
||||
exactly one `S*` label. An infrastructure-only issue is the deliberate exception:
|
||||
it may carry no `S*` label while being implemented, enters the common flow at
|
||||
`S7-code-review`, and from then on carries exactly one. Labels are the whole of
|
||||
it: GitHub Projects is no longer used.
|
||||
|
||||
**The light track is the default, not a shortcut** (owner's decision 2026-08-27,
|
||||
issue #338). `small` — the spec lives in the issue body and its review is a
|
||||
comment. Analysis names the `small` criterion the task *fails* when it takes the
|
||||
full track; "ordinary track" without a named criterion is not a justification.
|
||||
The threshold itself did not move — only which side carries the proof. The full
|
||||
track stays what it was for geometry, config migrations and public contracts,
|
||||
where a criterion is broken plainly and saying which one is easy.
|
||||
|
||||
`trivial` — the short track: no spec stage at all, `S2-analysis` straight to
|
||||
`S5-ready`, with the AC written into the issue body first. `trivial` requires a bug confined to one surface with no new UX
|
||||
contract, no migration, no i18n, no perf or touch impact, at most three checkable
|
||||
AC, **and expected behaviour already on record** — nothing left to decide. Code
|
||||
review is never skipped on either track: it checks scope, risks and the evidence
|
||||
from executed tests, but does not stand in for executing them.
|
||||
`PROCESS.md` §5 and §5.1 hold the criteria.
|
||||
|
||||
An issue filed by an outsider is worked exactly like one of the owner's own, once
|
||||
the owner has decided to take it. For product work the check sits **at the
|
||||
entrance**, not on every step: the first status label admits it to the flow. For
|
||||
infrastructure work an explicit assignment by the owner is the entrance, and the
|
||||
issue may remain without an `S*` label until its first `S7-code-review`. In either
|
||||
case, **who filed it stops mattering** once the owner has admitted it.
|
||||
|
||||
Applying that first label *is* the owner's explicit decision, and the platform
|
||||
already guarantees it — only someone with write access can label. The earlier rule
|
||||
made outside reports be refiled as the owner's own issues, which turned out to be
|
||||
work for nothing: on #123 the spec was already written by the time the guard
|
||||
refused.
|
||||
|
||||
Specs, audits and ADRs may live under `docs/`, but must link to their issue and
|
||||
must not become a parallel task list. When repository documentation disagrees with
|
||||
Issues, the issue wins.
|
||||
Where the rest lives: commands — `package.json` scripts, `CONTRIBUTING.md`,
|
||||
`docs/DEVELOPMENT.md` (toolchain, build, release — its Release section is the
|
||||
only home of release mechanics); tests and gates — `docs/TESTING.md`.
|
||||
|
||||
## Rule #1
|
||||
|
||||
@@ -101,547 +63,149 @@ gh issue view <NN> --repo Matysh/houseplan-card --json number,state,labels
|
||||
|
||||
The label must be one of `S5-ready`, `S6-in-progress`, `S7-code-review`. Anything
|
||||
else — refuse and say why. "Issue #83 is in `S2-analysis`, code is off limits.
|
||||
Start with the spec?" is the correct answer, not a smaller patch.
|
||||
Start with the spec?" is the correct answer, not a smaller patch. The one
|
||||
exception is a local, unpushed draft on `track:ask` while the spec is in
|
||||
`S4-spec-review`, each commit carrying the `Spec-Draft` trailer from the task
|
||||
packet; it reaches the branch only after a green spec review (`PROCESS.md`
|
||||
§11.8).
|
||||
|
||||
## Change classes
|
||||
GitHub Issues are the canonical task records and the **labels** are the status
|
||||
(`PROCESS.md` §9); when repository documentation disagrees with an issue, the
|
||||
issue wins. Change classes (`PROCESS.md` §1): **A** product (`src/**`,
|
||||
integration Python, manifests, i18n), **B** gates and tooling (tests, `demo/**`,
|
||||
`scripts/**`, `.github/**`, build and package configuration), **C**
|
||||
documentation, **D** generated (bundle, golden baselines) — D beats A where paths
|
||||
overlap. The committed bundle changes only in a commit with a `Release:` trailer
|
||||
(#657); an ordinary task restores it with `npm run bundle:clean` before
|
||||
committing.
|
||||
|
||||
| Class | Paths | Issue required |
|
||||
|---|---|---|
|
||||
| **A — product** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, i18n, `custom_components/**/translations/**` | yes |
|
||||
| **B — gates and tooling** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, `.github/workflows/**`, `rollup.config.mjs`, `tsconfig*.json` | yes; may reuse the issue it covers |
|
||||
| **C — documentation** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md` | not if it is part of its issue's DoD |
|
||||
| **D — generated** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` | never changes on its own. The stand copy `demo/srv/assets/**` is no longer committed (#255): build it with `npm run bundle:sync`. The bundle (`dist/**`, `custom_components/houseplan/frontend/**`) changes only in a commit with a `Release:` trailer — the beta/release candidate, `npm run bundle:release` (#657); an ordinary task leaves it behind the sources and restores the build with `npm run bundle:clean` before committing |
|
||||
**Tracks** (`PROCESS.md` §5, §5.1): the label `track:ship`, `track:show` or
|
||||
`track:ask` sets the route, and the owner's label beats the criteria. `show` is the
|
||||
default: up to three AC in the issue body, no spec review, `S2` → `S5`. `ship` is a
|
||||
one-sentence change within fixed limits, `S1` → `S5`. `ask` is the full route with
|
||||
a spec review. Any agent may raise a track with a reason; only the owner lowers it.
|
||||
A label is a proposal until the owner confirms it with a comment line
|
||||
`Трек: <ship|show|ask> — решение владельца`; an agent never writes that line.
|
||||
Several track labels at once read as the strictest. A risky changed hunk
|
||||
(geometry, touch, migration, devices, perf, a new UX key) raises an unconfirmed
|
||||
`ship` to `show` at `S7`; on `show` it is a question to the reviewer (#707).
|
||||
`small` and `trivial` read as `show`. An **infrastructure** task — not a single
|
||||
class A file — skips analysis and spec and enters at `S7-code-review`
|
||||
(`PROCESS.md` §1). Every change is code-reviewed; on `ship` the review moves to a
|
||||
batch review (`ship-review.yml`, `PROCESS.md` §11.7): nightly
|
||||
(`SHIP-REVIEW-<base>-dev-<sha12>.md`), and before the beta tag whatever the night
|
||||
did not read (`SHIP-REVIEW-<tag>.md`). The review pipeline prices each round by
|
||||
track (§10.4): no mutants run
|
||||
during development on any track — the whole registry runs nightly (#709); a
|
||||
rebase before review only on `ask` or when
|
||||
the branch does not merge cleanly into `dev`. Review checks scope, risks and the
|
||||
evidence from executed tests, but does not replace executing them.
|
||||
|
||||
The table above is a summary; `PROCESS.md` §1 is the authority and now covers the
|
||||
configuration files this one omits — `package.json`, `package-lock.json`,
|
||||
`pytest.ini`, `.gitignore`, `.gitattributes`, `.githooks/**` and the rest of
|
||||
`.github/**` are class B. Where paths overlap, **D beats A**: the built bundle
|
||||
lives inside `custom_components/houseplan/frontend/` and would otherwise read as
|
||||
product source.
|
||||
## Specs
|
||||
|
||||
## Commits
|
||||
The spec lives in the **issue body**, under a `## ТЗ` heading (owner decision
|
||||
2026-09-10, #517); `docs/specs/` is an archive of specs written before that date
|
||||
and takes no new files. Required sections and the rule for questions are
|
||||
`PROCESS.md` §7.1: only **product** ambiguity goes to the owner — what a person
|
||||
sees or does, and how much user-visible change belongs in this issue — in one
|
||||
batched comment with a proposed default for each question and `blocked` on top of
|
||||
`S3-spec`. Everything a user cannot observe is yours to decide and record.
|
||||
|
||||
Hooks install themselves: `package.json` runs `"prepare": "node
|
||||
scripts/install-hooks.mjs"`, so `npm ci` sets `core.hooksPath` in every fresh
|
||||
clone. Verify with `git config core.hooksPath` — expect `.githooks`.
|
||||
## Commits and branches
|
||||
|
||||
Every non-merge commit carries **terminal** trailers:
|
||||
Hooks install themselves on `npm ci` (`prepare` → `scripts/install-hooks.mjs`);
|
||||
`git config core.hooksPath` must print `.githooks`. Every non-merge commit that
|
||||
touches anything outside class C (docs) carries **terminal** trailers; a
|
||||
docs-only commit needs none (`PROCESS.md` §3 п.10, #701):
|
||||
|
||||
```text
|
||||
Issue: #123
|
||||
User-Visible: yes
|
||||
```
|
||||
|
||||
One `Issue:` line per issue if a commit closes several. `User-Visible: no` for
|
||||
tests, refactors, tooling and documentation that does not change the product.
|
||||
`User-Visible: yes` requires edits to **both** changelogs — `docs/CHANGELOG.md`
|
||||
and `docs/CHANGELOG.ru.md` — in the same commit.
|
||||
One `Issue:` line per issue. `User-Visible: yes` requires edits to **both**
|
||||
`docs/CHANGELOG.md` and `docs/CHANGELOG.ru.md` in the same commit. A commit
|
||||
touching `demo/golden/baselines/**` also needs `Release:` plus exactly one of
|
||||
`Baseline-Reviewed: <GitHub run URL>` or `Baseline-Reviewed-Local: sha256:<WSL
|
||||
attestation>` (`PROCESS.md` §10.1). Never invent a review link and never rewrite
|
||||
published history to satisfy trailers.
|
||||
|
||||
A commit touching `demo/golden/baselines/**` additionally requires:
|
||||
Branch `issue/<NN>-slug`; direct commits to `dev`, no PR (owner's decision); a
|
||||
violation is fixed with a follow-up commit, never a force-push.
|
||||
|
||||
```text
|
||||
Release: v1.62.0-beta.9
|
||||
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/<run-id>
|
||||
```
|
||||
|
||||
or, for a complete attested capture made in the repository's WSL/ext4 clone:
|
||||
|
||||
```text
|
||||
Release: v1.62.0-beta.9
|
||||
Baseline-Reviewed-Local: sha256:<wsl-attestation hash>
|
||||
```
|
||||
|
||||
Exactly one of the two `Baseline-Reviewed*` trailers is allowed. The local hash
|
||||
must equal `localAttestation.sha256` in the committed baseline index; it does not
|
||||
replace the mandatory full GitHub Validate on the final exact SHA.
|
||||
|
||||
Never invent a review link and never rewrite published history to satisfy
|
||||
trailers. `.githooks/commit-msg` and the `provenance` CI job both run
|
||||
`scripts/validate-commit-provenance.mjs`.
|
||||
|
||||
Branch: `issue/<NN>-slug`. Direct commits to `dev`, no PR — the owner's decision;
|
||||
CI checks after the fact, and a violation is fixed with a follow-up commit, never
|
||||
a force-push.
|
||||
|
||||
**Push after every task, not before a beta.** While work sits unpushed there is
|
||||
nothing to review, and reviewing twenty tasks at once is not review. `dev` may hold
|
||||
unreviewed code while a task is in flight; what matters is its state when the
|
||||
reviewer says it is accepted.
|
||||
|
||||
**Standing permission: push `issue/<NN>-slug` without asking.** The reviewer runs
|
||||
in CI and can only read what is on the remote — an unpushed spec or commit means
|
||||
the review either stalls or judges the wrong tree. Pushing a task branch publishes
|
||||
nothing to users and does not touch the integration branch, so it needs no command.
|
||||
|
||||
**Do not merge into `dev` by hand.** On a green code review the pipeline rebases
|
||||
the task branch onto `dev`, pushes it, and only then sets `S8-merged` — the label
|
||||
asserts the code is in `dev`, so the merge has to happen first or the label lies
|
||||
in between.
|
||||
|
||||
If the rebase conflicts the pipeline says so in the issue and sends the task back
|
||||
to `S6-in-progress`. The verdict still stands: nothing needs reviewing again, the
|
||||
remaining work is the rebase. When the conflict is only in generated files, run
|
||||
`node scripts/rebase-on-dev.mjs` (#479): a conflict in the committed bundle
|
||||
(`dist/**`, `custom_components/houseplan/frontend/**` — possible only for a branch
|
||||
started before #657, which stopped tasks from committing it) takes `dev`'s copy
|
||||
and continues, with no rebuild and no amend; a conflict in `docs/reviews/INDEX.md` is
|
||||
rebuilt from the directory (#643, the same helper the pipeline uses, so the
|
||||
pipeline no longer bounces a task on it); a conflict anywhere else aborts and
|
||||
leaves the tree as it was. Then push the branch and re-apply `S7-code-review`. When the
|
||||
only difference from the reviewed material is the pipeline's own review-document
|
||||
commit, the next run re-applies the green verdict without calling the model
|
||||
(#499); any other change to the tree — a rebase included — gets a full review.
|
||||
|
||||
The pipeline is an idempotent controller (#499): only `S4-spec-review` and
|
||||
`S7-code-review` start it, it reads the issue's *current* labels rather than the
|
||||
event snapshot, and a label removed before the run starts is treated as a
|
||||
withdrawn request. Push the material **before** applying the label — the reviewer
|
||||
is pinned to the SHA the pipeline captured and must not fetch newer commits. The second review run is not a formality — after a rebase onto a
|
||||
moved `dev` this is different code, and accepting it unchecked is how regressions
|
||||
arrive. Cycles are counted per stage, so a code review spends its own budget.
|
||||
|
||||
Everything else still requires the owner's explicit command: pushing `main`,
|
||||
creating tags, publishing betas and releases, closing issues.
|
||||
- **Push after every task, not before a beta** — while work sits unpushed there
|
||||
is nothing to review.
|
||||
- **Standing permission: push `issue/<NN>-slug` without asking.** The reviewer
|
||||
runs in CI and reads only the remote; a task branch publishes nothing to users.
|
||||
Push the material **before** applying the review label.
|
||||
- **Do not merge into `dev` by hand.** On a green review the pipeline rebases,
|
||||
pushes and only then sets `S8-merged`. A conflicting rebase returns the task to
|
||||
`S6-in-progress` with the verdict intact; `node scripts/rebase-on-dev.mjs`
|
||||
resolves conflicts in generated files only (`PROCESS.md` §2.10), then push and
|
||||
re-apply `S7-code-review`.
|
||||
- Everything else needs the owner's explicit command: pushing `main`, tags,
|
||||
publishing betas and releases, closing issues.
|
||||
|
||||
## Working trees (#115)
|
||||
|
||||
One checkout, one `HEAD`: two agents sharing a directory inherit each other's
|
||||
branch, and twice in one hour a commit landed on someone else's task branch that
|
||||
way. The layout is therefore fixed:
|
||||
branch. `houseplan-card-src/houseplan-card` is the author's tree — task branches
|
||||
live there, and unfamiliar local changes belong to the author or the owner,
|
||||
never reset or clean them away. `houseplan-card-src/hp-dev` is the owner's
|
||||
worktree, permanently on `dev`. The reviewer owns no tree: it runs in CI on a
|
||||
fresh checkout. A worktree works only on the machine that created it — its
|
||||
`.git` file records an absolute path in that machine's format.
|
||||
|
||||
- **`houseplan-card-src/houseplan-card`** — the author's tree. Task branches live
|
||||
here; nobody else commits in it. Unfamiliar local changes belong to the author
|
||||
or the owner — never reset or clean them away.
|
||||
- **`houseplan-card-src/hp-dev`** — the owner's worktree, permanently on `dev`. For owner-side operations that must not disturb the
|
||||
author's tree: pushing `dev`, restoring a hook's executable bit, emergencies.
|
||||
- **The reviewer owns no author tree.** It runs in CI on a fresh checkout. The
|
||||
agent implementing an infrastructure task is an ordinary task author and uses
|
||||
the same author-tree rules as product work; task branches must not share a
|
||||
mutable checkout concurrently.
|
||||
## Handoff and the verdict
|
||||
|
||||
A worktree is only usable on the machine that created it: the `.git` file records
|
||||
an absolute path in that machine's format. One created from a Linux sandbox is
|
||||
dead on Windows and vice versa — create worktrees on the machine that will use
|
||||
them, which for `hp-dev` means the owner's.
|
||||
Start a task from its packet: `node scripts/task-packet.mjs --issue NN` (status;
|
||||
track with its basis, cycle limit and rebase policy; what the status permits;
|
||||
the branch against `dev` and the next step — no rebase a clean `show`/`ship`
|
||||
merge does not need; risk by changed hunks and what it means on this track; the
|
||||
required checks, each with its reason; changelog and visual evidence; the
|
||||
previous verdict and the unwitnessed AC; it writes nothing). The local gate is
|
||||
`npm run gate:small` (`docs/TESTING.md` › Локальный набор перед пушем); run the
|
||||
smokes named in the AC before `S7-code-review`. **"Verified" without a named
|
||||
command and its result is not evidence.** Comment formats — claim, handoff,
|
||||
verdict — are `PROCESS.md` §7.2.
|
||||
|
||||
## Agent-neutral workflow
|
||||
**One handoff, one push** (`PROCESS.md` §10.4). Run
|
||||
`node scripts/process-gate.mjs --issues` before pushing; after
|
||||
`S7-code-review` do not push to the branch until the verdict or the return
|
||||
arrives — a push on top of a running review cancels it.
|
||||
|
||||
No task type is reserved for Codex, Claude or any other named model. **Any agent
|
||||
may take any task**: analysis, spec, product implementation, infrastructure or a
|
||||
release explicitly commanded by the owner. Roles describe the current artifact,
|
||||
not the agent brand. The owner rules on product disputes, closes issues and
|
||||
commands releases.
|
||||
|
||||
Author and reviewer are independent agents/sessions. They need not use different
|
||||
model families, but the reviewer must start without implementation context and
|
||||
must not be the author grading their own work. The reviewer does not edit the
|
||||
material under review.
|
||||
|
||||
**Infrastructure-only work uses an accelerated entry into the common flow.** It
|
||||
is implemented immediately by any agent, without analysis, spec, spec review or
|
||||
the statuses `S1`…`S6`. Once the branch is ready and pushed, apply
|
||||
`S7-code-review`. From there the ordinary controller applies: green review rebases
|
||||
and merges the checked material into `dev` and then sets `S8-merged`; findings or
|
||||
a failed merge return the issue to `S6-in-progress`, and after correction it is
|
||||
submitted to `S7-code-review` again.
|
||||
|
||||
The test for "infrastructure only" is mechanical: **not a single class A file** —
|
||||
nothing under `src/**`, no `custom_components/**/*.py`, no manifests, no i18n. A
|
||||
task that touches class A even once is not infrastructure and takes the full flow;
|
||||
there is no such thing as "mostly infrastructure". The strictness is deliberate:
|
||||
a loose reading would turn this into the route by which product changes skip
|
||||
review.
|
||||
|
||||
What stays mandatory either way: an issue exists, both trailers are on every
|
||||
commit, proportionate local gates are green (normally `typecheck`, `test` and
|
||||
`build`), and any non-obvious decision is written down in the code or the issue
|
||||
rather than kept in someone's head. Infrastructure work skips specification, not
|
||||
code review.
|
||||
|
||||
**Review starts by itself.** Applying `S4-spec-review` or `S7-code-review` fires the
|
||||
pipeline. Deterministic gates, model review and integration have independent
|
||||
55/45/55-minute budgets (#551); typical runs finish well before those ceilings.
|
||||
|
||||
**Having applied one of those labels, wait for the result instead of ending the
|
||||
session.** Reporting "handed over for review" stops a conveyor that could have kept
|
||||
moving on its own. An agent has no clock — it exists only during its own turn — so
|
||||
waiting means polling: every 90 seconds, at most 110 times. A single long sleep hits
|
||||
the command timeout. Do the polling with `node scripts/wait-verdict.mjs --issue NN
|
||||
[--sha <tip>]` (#496): it watches the label, the pipeline's own comments (conflict,
|
||||
cancelled merge, failed run) and optionally Validate on the SHA, prints only when
|
||||
the state changes and exits 0 on a new label, 3 on an event that needs a hand,
|
||||
4 on timeout — the same 90 s × 110 without a model turn per tick. It writes
|
||||
nothing. Pipeline comments older than the latest application of `S4`/`S7` are
|
||||
the baseline, not an outcome of the new round; an outcome from the current round
|
||||
which already exists when the waiter starts is still delivered immediately (#546).
|
||||
Watch the **label**, not the comment: the label is the state,
|
||||
the comment only explains it. Do not wait at all while `blocked` is set — the task
|
||||
is waiting on the owner, not on the reviewer. On exhausting the attempts, stop and
|
||||
tell the owner: a failed run leaves the label where it was, forever.
|
||||
|
||||
What the new label means:
|
||||
**Having applied `S4-spec-review` or `S7-code-review`, wait for the result
|
||||
instead of ending the session.** The label starts the pipeline by itself. Poll
|
||||
with `node scripts/wait-verdict.mjs --issue NN [--sha <tip>]` (#496): it watches
|
||||
the label and the pipeline's comments every 90 s, at most 110 times, prints only
|
||||
on a change and exits 0 on a new label, 3 on an event that needs a hand, 4 on
|
||||
timeout. Watch the **label**, not the comment. Do not wait while `blocked` is
|
||||
set.
|
||||
|
||||
| Now reads | What happened | What you do |
|
||||
|---|---|---|
|
||||
| `S5-ready` | the spec is accepted | write the code |
|
||||
| `S5-ready` | the spec is accepted | write the code (a draft from `S4`: rebase onto `dev`, check, push — §11.8) |
|
||||
| `S3-spec` | the spec came back | read the verdict, revise, re-apply `S4-spec-review` |
|
||||
| `S6-in-progress` | the code came back | revise, re-apply `S7-code-review` — **or**, if the verdict was green and only the merge conflicted, just rebase and re-apply. The comment says which |
|
||||
| `S8-merged` | accepted and already in `dev` | nothing |
|
||||
| `review-4` | the cycle limit is spent | stop, the owner decides |
|
||||
|
||||
**After a review run the label always changes.** If it did not, the run itself
|
||||
failed rather than the work — say so to the owner instead of polling on.
|
||||
failed rather than the work — say so to the owner instead of polling on. The
|
||||
bounded queue reconciler (#555) re-wakes a review whose event was lost; it is a
|
||||
safety net, not permission to stop waiting for the review you started.
|
||||
|
||||
The repository also has a bounded queue reconciler (#555). It takes one S4/S7
|
||||
snapshot every thirty minutes and exits. It may re-apply the same review label
|
||||
only when the matching event was lost or its run ended with a transient
|
||||
cancellation/timeout before a sealed result existed. It never applies verdicts or
|
||||
merges. Running work, `blocked`, `review-4`, a foreign material/stage/attempt, a
|
||||
guard failure, or an unintegrated sealed model result is left untouched and gets
|
||||
at most one machine-keyed diagnostic. This is a safety net, not permission for an
|
||||
agent to stop waiting for the result of the review it started.
|
||||
|
||||
**A failed pre-release gate does not send the issue back to review.** The
|
||||
implementation loop runs only typecheck, unit and build; golden, browser smokes,
|
||||
performance and the full HA harness run before a beta, which is after the code
|
||||
review has passed and the issue sits in `S8-merged`. Some defects cannot surface
|
||||
any earlier.
|
||||
|
||||
Fix it, re-run what failed, and a green run is enough for the release to continue.
|
||||
The issue stays in `S8-merged`. Record the **exact command and its result** in the
|
||||
issue — "verified" without a command proves nothing. Trailers as usual, and
|
||||
`User-Visible: yes` still means both changelogs in the same commit.
|
||||
|
||||
The exception covers repairing the defect the gate named, not carrying on
|
||||
development under the name of a repair. It goes through the normal flow — a new
|
||||
issue, or back to `S6-in-progress` — if the fix changes a behaviour contract, gives
|
||||
the user something new, reaches a subsystem the task never touched, or is
|
||||
comparable in size to the task itself. And editing the gate so it stops failing is
|
||||
concealment, not repair; the exception is a defect proven to be **in the fixture**,
|
||||
as on #89, where the sun sat at azimuth 180° and the only window faced north, so no
|
||||
ray was ever built.
|
||||
|
||||
Baselines are still accepted only via `npm run golden:accept -- --reviewed` from
|
||||
either a complete Linux CI artefact or a complete attested WSL artefact produced
|
||||
by `npm run golden:wsl:capture`. "So the gate goes green" is not a reason, and
|
||||
the WSL route never replaces the independent full GitHub Validate on the final
|
||||
exact SHA.
|
||||
|
||||
The exchange happens in **issue comments** — there is no local message bus. Verdict
|
||||
format:
|
||||
|
||||
```text
|
||||
Verdict: green/yellow/red · cycle r<N>/4 · High: N · Medium: N → in-task | #… · Document: …
|
||||
```
|
||||
|
||||
High blocks. A Medium finding INSIDE the task's scope is fixed within the task:
|
||||
with no High findings the verdict is yellow, the author fixes it and the fix
|
||||
passes another review cycle — no separate issue (owner's decision 2026-08-19,
|
||||
#202: filing and servicing an issue costs far more than fixing in place). Only
|
||||
a Medium finding OUTSIDE the scope becomes its own issue — foreign scope is
|
||||
never patched from this branch. Low is fixed or waived with a note
|
||||
in the review document. A yellow verdict is legitimate even when every acceptance
|
||||
criterion passes, if the change does not solve the stated scenario or degrades a
|
||||
neighbouring one.
|
||||
|
||||
**Four review cycles** (two on the light track). The counter lives in the document
|
||||
name, `-r1`…`-r4`; the fourth adds the `review-4` label. There is no fifth attempt:
|
||||
the owner splits the task, rejects it, or arbitrates.
|
||||
|
||||
On the light track (`small`: complexity ≤3, one surface, no config migration, no
|
||||
new UX contract, no perf or touch impact — all at once) the spec lives in the issue
|
||||
body and the spec review is a comment. Code review is never skipped. This track is
|
||||
the default: taking the full one means naming the criterion above that the task
|
||||
does not meet.
|
||||
|
||||
## Specs
|
||||
|
||||
The spec lives in the **issue body**, under a `## ТЗ` heading (owner decision
|
||||
2026-09-10, #517); `docs/specs/` is an archive of specs written before that date
|
||||
and takes no new files. Required sections are in `PROCESS.md` §7.1, plus two
|
||||
product ones: which persona meets this, on which surface, at what moment; and what
|
||||
the person sees before and after, in one sentence without implementation terms.
|
||||
Proof that a verdict was passed on a given text is the pipeline's job: it writes
|
||||
the `sha256` of the normalised body into the review document's anchor block, and
|
||||
an edit made after a green spec review reaches the code reviewer as a finding.
|
||||
|
||||
**Ambiguity is asked, not guessed — but only product ambiguity.** A guess written as
|
||||
fact is the worst kind of defect: it passes review because it looks like a decision.
|
||||
|
||||
The owner answers exactly two kinds of question: **what a person sees or does**, and
|
||||
**how much user-visible change belongs in this issue**. Behaviour in a boundary case,
|
||||
which persona wins when two conflict, what counts as acceptable degradation, whether
|
||||
a neighbouring behaviour is in scope here or becomes its own issue.
|
||||
|
||||
Everything a user cannot observe is yours to settle: where state is stored, which
|
||||
module carries the guard, naming, file layout, test strategy, migration mechanics,
|
||||
development policy. Decide it, record it in an explicit "assumed, change freely"
|
||||
block, and let the reviewer challenge it. A technical disagreement between author and
|
||||
reviewer is settled by the verdict, not by the owner; it reaches him only when the
|
||||
cycle limit is exhausted.
|
||||
|
||||
Split a mixed question instead of escalating all of it. "Where does this state live"
|
||||
is technical. "Does it survive a page reload and follow the plan across screens" is
|
||||
product. Ask the second, decide the first.
|
||||
|
||||
Ask in one batched issue comment, each question carrying a proposed default, and put
|
||||
`blocked` on top of `S3-spec` while waiting. A question with a default costs the
|
||||
owner seconds; one without costs him minutes.
|
||||
|
||||
## Gates
|
||||
|
||||
```
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build
|
||||
npm run inventory # the only correct way to get test counts
|
||||
```
|
||||
|
||||
Never copy test counts into documents by hand; they go stale in days.
|
||||
|
||||
After building, lay the bundle out for the stand; only a candidate updates the
|
||||
committed copy (#657):
|
||||
|
||||
```
|
||||
npm run bundle:sync # build + dist → demo/srv/assets (#255)
|
||||
npm run bundle:clean # before an ordinary commit: dist back to the committed copy (#657)
|
||||
npm run bundle:release # candidate only: build + dist → custom_components + demo/srv/assets
|
||||
npm run bundle:budget # initial View graph within INITIAL_VIEW_GZIP_BUDGET (scripts/bundle-budget.mjs, #337/#367)
|
||||
```
|
||||
|
||||
CI (`bundle-policy --verify`) checks the fresh build's integrity on every push
|
||||
and compares it byte-for-byte with the committed copy only where the commit
|
||||
changes the bundle or is a candidate. Publishing a beta additionally refuses a
|
||||
committed bundle whose embedded source fingerprint differs from the tree. The
|
||||
dev stand gets the head of `dev` from the Validate artifact via the `dev-build`
|
||||
branch, not from the tree (`demo/stand/README.md`).
|
||||
|
||||
`npm run gate:small` runs the mandatory part of PROCESS §8 in one go (#479,
|
||||
#576): build with typecheck, `no-new-any`, `no-new-private-writes` and `smoke-select` start in parallel;
|
||||
unit tests follow the completed build because their bundle-contract witnesses
|
||||
read the freshly produced `dist`, then the bundle-tree comparison and the
|
||||
bundle budget run. It prints the smokes the
|
||||
diff selects but does not run them by default — `npm run gate:small -- --smokes`
|
||||
(#496) adds the browser phase after the artefact preparation: `bundle-sync`, then
|
||||
the directly matched and registered smokes two at a time; "broad" matches stay
|
||||
the reviewer's call. `golden`, `pytest` and `check-docs --screenshots=strict`
|
||||
remain the author's call by diff and AC.
|
||||
|
||||
**Start a task from its packet** (#496): `node scripts/task-packet.mjs --issue NN`
|
||||
prints one derived view — status and track, what the status permits, the owner's
|
||||
recent decisions, the branch against `dev` and Validate on its tip, the previous
|
||||
verdict with its recorded tree, AC → evidence from the last review document and
|
||||
what is still unwitnessed. It reads GitHub and git and writes nothing; the labels
|
||||
remain the only source of status.
|
||||
|
||||
**Heavy CI gates run on the beta candidate, nightly and on demand — not on every
|
||||
push (#479).** `smoke`, `golden` and `performance_smoke` in Validate are gated
|
||||
on the `heavy` output: true for a head commit carrying a `Release:` trailer, for
|
||||
`workflow_dispatch full=true` (which `nightly.yml` issues on `dev` every night)
|
||||
and for pull requests. A plain push to `dev` runs preflight, frontend (types,
|
||||
units, build, bundle sync, no-new-any, no-new-private-writes), the narrow TS/Python geometry parity
|
||||
guard when its inputs changed, backend, hacs and hassfest. Screenshot
|
||||
freshness in `check-docs` is likewise a warning on a plain push and an error on
|
||||
the candidate; `publish-prerelease.yml` and `release.yml` refuse a candidate
|
||||
without the `Release:` trailer, so a green Validate without the heavy jobs can
|
||||
never pass for a release.
|
||||
|
||||
During the implementation cycle the fast gates always run. Since 2026-08-14 the
|
||||
owner's machine also carries Playwright with Chromium (Windows) and a full WSL
|
||||
environment, which changes one thing (#151): **before moving an issue to
|
||||
`S7-code-review`, run the smokes named in its AC locally** — `node
|
||||
demo/smoke_<name>.mjs`. A red smoke that reaches the review costs a cycle; run
|
||||
locally it costs a minute. Precedent: on #89 a fixture error lived through a
|
||||
whole review round that a local run would have caught immediately.
|
||||
|
||||
**A smoke enters through the public surface (#629)**: contract `data-hp` hooks,
|
||||
HA/fixture events and the harness facade `window.__hpTest`
|
||||
(`docs/TESTING.md`); private card fields are read-only in assertions, and a new
|
||||
write needs `// private-ok: <reason>` or the `no-new-private-writes` gate fails.
|
||||
|
||||
**One handoff, one push (#510).** Run `node scripts/process-gate.mjs --issues`
|
||||
locally with `gh` available before pushing (without `gh` the hook cannot check the
|
||||
issue status and stays silent). After `S7-code-review` do not push to the branch
|
||||
until the verdict or the return arrives: a push on top of a running review cancels
|
||||
it (10–20 runner minutes) and, after the material is fixed, also the merge (#312).
|
||||
Set `S7` once per round, not after every CI fix: the pipeline now runs Validate
|
||||
with the diff mutants on the material itself and returns a red one to `S6` without
|
||||
spending a review cycle; since #636 it does not sleep while Validate runs — the
|
||||
prepare stage leaves a sealed `review-pending` marker and exits, and the
|
||||
completion of Validate (`process-resume.yml`, `workflow_run`) re-applies `S7`
|
||||
so a fresh run finds the finished dispatch; `process-reconcile.yml` is the
|
||||
fallback for a lost event. A second `S7` from the pipeline itself is therefore
|
||||
normal and is not a new round. Mutants by diff run only where they are explicitly
|
||||
requested — the review candidate, the merge candidate (both dispatch Validate
|
||||
with `mutants=true`) and PRs (#510, #601). Ordinary pushes, the beta candidate
|
||||
(`Release:` trailer) and `full=true` do not request them: a routine push costs
|
||||
~3 minutes (08–09.09 mutants cost 48 of 56 Validate job-hours and were mostly
|
||||
cancelled by the next push), and by the beta every issue has already been
|
||||
mutated twice — on review and on the rebased merge candidate; the night runs
|
||||
the full registry (`mutation-gate.yml`), not a diff subset.
|
||||
|
||||
**Workflows run from the default branch are thin callers (#623).** For
|
||||
`issues`, `schedule` and `workflow_run` GitHub executes the file from `main`.
|
||||
The six such files (`process.yml`, `process-resume.yml`,
|
||||
`process-reconcile.yml`, `mutation-gate.yml`, `nightly.yml`,
|
||||
`process-metrics.yml`) carry only triggers, run-name, permissions and
|
||||
concurrency and call their body `_<name>.yml` at `@dev` with
|
||||
`secrets: inherit`. A pipeline change is one commit to `dev` — edit the
|
||||
`_<name>.yml` body; no mirror into `main`, no merge-back before promotion.
|
||||
Only a change of triggers, dispatch inputs or the permission ceiling touches the
|
||||
thin file, and then it is mirrored into `main` (preflight `workflow_sync`
|
||||
compares exactly these six; `PROCESS.md` §10.4).
|
||||
|
||||
The full smoke set, `golden` and `performance_smoke` still belong to the
|
||||
pre-beta run — which is then mandatory and complete. WSL runs of the full HA
|
||||
harness (`~/houseplan-card`, venv) are advisory; **the canon does not move**:
|
||||
the beta gate is CI at the exact SHA.
|
||||
|
||||
**Verifying and capturing are different things (#455).** `golden:verify` is
|
||||
advisory and legal anywhere, Windows included: it reports differences and
|
||||
accepts nothing. **Capturing** frames is refused outside Linux
|
||||
before the browser even starts — `golden:capture` through
|
||||
`demo/golden/policy.mjs`, documentation screenshots through
|
||||
`npm run docs:capture` (use that script, not a bare `node
|
||||
demo/docs/capture.mjs`: the gate sits one step earlier because editing the
|
||||
capture script invalidates the committed screenshot index). The refusal prints
|
||||
the WSL command. The reason is not policy but physics: Windows
|
||||
rasterizes text through DirectWrite with different subpixel and DPI behaviour,
|
||||
so no frame ever matches an accepted baseline byte for byte, no environment
|
||||
witness can exist, and acceptance would refuse anyway (#401 accepts any
|
||||
environment that proves itself with byte-identical undeclared frames — Linux is
|
||||
simply the only one we have). The deliberate override is
|
||||
`HP_ALLOW_FOREIGN_CAPTURE="reason"`; the reason travels into the output and the
|
||||
manifest. Baselines are still accepted only via
|
||||
`npm run golden:accept -- --reviewed` on a complete artefact. A local Linux
|
||||
capture additionally requires the self-hashed WSL passport produced by
|
||||
`npm run golden:wsl:capture`; plain `golden:capture` remains diagnostic. The
|
||||
passport proves a clean published branch SHA, WSL/ext4, pinned toolchain,
|
||||
current fingerprint, complete matrix, PNG hashes and witness floor. The
|
||||
accepted index records that local provenance separately from a GitHub run,
|
||||
and a final full GitHub Validate on the accepted exact SHA remains mandatory.
|
||||
The one local
|
||||
shortcut is `npm run docs:accept -- --identical` (#512): it re-captures on this
|
||||
machine, compares decoded pixels with the committed frames and, only when every
|
||||
frame is identical, refreshes the manifest fingerprints — frames that differ go
|
||||
through the artefact as before. The displayed card version reaches the DOM
|
||||
through `displayVersion()` (`src/card-version.ts`); the global
|
||||
`__HP_VERSION_OVERRIDE__` behind it is for harnesses only and the product never
|
||||
sets it.
|
||||
|
||||
**Backend.** A full Home Assistant harness cannot run on native Windows at all:
|
||||
Home Assistant imports the Unix-only `fcntl` module. Its canon is Linux CI or WSL.
|
||||
Locally only the pure subset runs; `python -m pytest tests_backend/ -q` without
|
||||
Home Assistant **does not collect** `test_ha_*.py` at all (`conftest.py`
|
||||
`collect_ignore_glob` when `homeassistant` is not importable — not a skip) and, since
|
||||
#630, prints `HA harness NOT collected: N files (M tests)` with the canon to run
|
||||
them. A green result there proves nothing about the harness. Say so in the report
|
||||
instead of claiming the backend was verified. Cloud agents have the harness
|
||||
at `.venv-backend/bin/python`.
|
||||
|
||||
**Running the app / smoke suite**: build a fresh bundle and copy it into the demo
|
||||
assets first, then run `node demo/smoke_*.mjs`. No real Home Assistant server is
|
||||
required: `demo/srv/demo.html` stubs `hass`, registries and `callService`.
|
||||
|
||||
**Golden images**: `npm run golden:capture` and `npm run golden:verify` refuse a
|
||||
stale demo bundle. Build and copy first, then review `artifacts/golden/actual/` and
|
||||
`diff/`. Update baselines only with `npm run golden:accept -- --reviewed`, using a
|
||||
complete Linux CI artifact or the attested artefact made in the WSL/ext4 clone by
|
||||
`npm run golden:wsl:capture`; never accept a partial scenario or images merely to
|
||||
make CI green. The local route saves the first expected-red CI round trip only:
|
||||
the accepted commit still needs a full GitHub Validate on its exact SHA. See
|
||||
`demo/golden/README.md`.
|
||||
|
||||
**Freshness contract**: the embedded fingerprint covers `src/` plus Rollup,
|
||||
TypeScript and package-lock build inputs. Every browser check must verify it
|
||||
before trusting a result — benchmarks, golden runs and documentation captures
|
||||
call `assertFreshDemoBundle` themselves, and smokes get it from `launch()` in
|
||||
`demo/serve.mjs` (#236). A missing or mismatched fingerprint is a hard failure,
|
||||
not a warning; `HP_ALLOW_STALE_BUNDLE=1` skips the check for debugging and says
|
||||
so out loud. A smoke against a stale bundle does not fail cleanly: part of its
|
||||
assertions go red and part stay green, which reads as a logic defect.
|
||||
|
||||
**CI is pinned to an exact SHA.** The release gate accepts only a `completed
|
||||
success` run for the candidate's SHA, not "the last green one"; a new push cancels
|
||||
an unfinished Validate for the same branch. Gate jobs, matching the actual
|
||||
`validate.yml` (#191): `docs`, `provenance`, `process-gate`, `hacs`, `hassfest`,
|
||||
`frontend`, `smoke`, `golden`, `performance_smoke`, `geometry_parity`, `backend`.
|
||||
The `changes` job
|
||||
is a service path-filter, not a gate. `docs` is a real blocker: it checks the
|
||||
screenshots `sourceFingerprint` against current `src/**`, which is exactly what
|
||||
went red after the #113 merge. A stable release additionally waits for Full
|
||||
Performance and for a green E2E run on a real Home Assistant (`houseplan-e2e`,
|
||||
dispatched on the candidate SHA by `release.yml`, #514/#540); betas and the
|
||||
development cycle never run E2E. Stable installable assets (`houseplan.zip`,
|
||||
`houseplan-card.js` and their `SHA256SUMS`) reach the public stable release only
|
||||
from `release.yml` after those gates; a release published by hand is turned back
|
||||
into a draft first (#540). The prerelease publisher additionally ships a
|
||||
candidate-bound `RELEASE-MEMBERSHIP.json` covered by the same passport (#547). Before
|
||||
every stable release `release.yml` also queues an independent review of the
|
||||
whole beta line (`release-review.yml`, `PROCESS.md` §11.5, #638): no specs, no
|
||||
review rounds, output `docs/reviews/RELEASE-REVIEW-vX.Y.Z.md`; it runs in
|
||||
parallel and never blocks the release.
|
||||
|
||||
**"Verified" without a named command and its result is not evidence.**
|
||||
A failed pre-release gate (golden, full smokes, performance, HA harness) does not
|
||||
send the issue back to review: fix, re-run what failed, record the exact command
|
||||
and result in the issue (`PROCESS.md` §11.4 — and its limits).
|
||||
|
||||
## Environments
|
||||
|
||||
**Local Windows checkout** is the day-to-day environment: repository-pinned Node
|
||||
and Python as in CI (`npm run toolchain:check` compares the machine with the pins
|
||||
CI actually uses — `.nvmrc` and `.python-version` are derived from the same
|
||||
sources, #496),
|
||||
`gh` authenticated. On the owner's machine do not trust the ambient PATH:
|
||||
`.\scripts\windows-toolchain.ps1 setup|check` owns a verified portable Node and
|
||||
dedicated `.venv-ci`, and its `npm`/`node`/`python`/`playwright` actions are the
|
||||
explicit pinned entrypoints (#557). It changes no persistent PATH and never
|
||||
deletes a mismatched venv. In WSL, work from an ext4 clone and use
|
||||
`bash scripts/wsl-setup.sh --verify` for the real HA subset plus one Linux visual
|
||||
capture; exact-SHA Linux CI remains authoritative. `.venv-backend` does **not**
|
||||
exist there — it is provisioned only by cloud agent startup scripts, which also
|
||||
run `npm ci` and install Playwright Chromium.
|
||||
|
||||
Known environment-sensitive smoke: `demo/smoke_opening_measure.mjs` fails two
|
||||
sub-checks (`place_dialog_x_magnetised`, `place_committed_x_center`) under the pinned
|
||||
Chromium — a `1e-6`-tolerance magnet-snap on the opening-*placement* path. It
|
||||
reproduces against the pristine committed bundle, so treat it as
|
||||
pre-existing/pixel-precision, not a regression you introduced.
|
||||
|
||||
## Alpha experiments
|
||||
|
||||
`src/labs.ts` is the single registry and resolver for hidden presentation
|
||||
experiments. One browser-local switch controls the complete set known to the
|
||||
installed build: `hp_alpha=1` in query or the shared hash grammar enables and
|
||||
persists it, while `hp_alpha=0` disables and persists it. Do not add per-feature
|
||||
URL/storage keys or a YAML/config switch. The legacy `hp-labs` grammar and
|
||||
`houseplan_card_labs_v1` storage are ignored and never migrated.
|
||||
|
||||
A new registry entry needs a unique lowercase id, issue, summary and
|
||||
unit/browser coverage; it does not get `since` or `expires`. Invalid or duplicate
|
||||
entries fail closed. Alpha capabilities may alter presentation only and must not
|
||||
gate data, migrations, stores, HA actions or network calls. Current renderer
|
||||
details are in `docs/ISOMETRIC.md`.
|
||||
|
||||
Demo harness render quirk: the fake `hass` in `demo.html` is set once, so opening the
|
||||
page directly in a browser renders the floor plan but **device icons only appear
|
||||
after a re-render** (an F5 refresh, or nudging `card.hass = {...card.hass}`). The
|
||||
smoke launcher `demo/serve.mjs` already does this nudge; a plain browser session does
|
||||
not. This is a harness limitation, not a card bug.
|
||||
|
||||
## Promotion rule
|
||||
|
||||
Every new feature or material behaviour change must be published as a beta/RC
|
||||
before it can enter a stable release, even when its local audit is clean. The
|
||||
stable release commit is promotion-only: version fields, generated bundle
|
||||
snapshots and changelog/release metadata. Do not add feature source code in
|
||||
that commit. An explicit owner-requested emergency hotfix is the only exception
|
||||
and must be called out in the release handoff.
|
||||
|
||||
A `Release vX.Y.Z-beta.N candidate` commit is **not** promotion-only: it carries
|
||||
the work itself and follows the ordinary rules, trailers included.
|
||||
|
||||
Issues are closed in a batch when a beta ships, not when implementation ends: that
|
||||
way a bug found in the beta returns to the same task, and the beta announcement can
|
||||
list what went in. The batch is immutable candidate membership proven by Git
|
||||
trailers, never the mutable S8 queue at close time; authorship does not change
|
||||
membership. Status labels are stripped as the issues close, and retries resume the
|
||||
same manifest without duplicating the release comment (#547).
|
||||
The owner's Windows machine: `.\scripts\windows-toolchain.ps1 setup|check` owns
|
||||
the pinned Node and Python; WSL works from an ext4 clone with
|
||||
`bash scripts/wsl-setup.sh --verify` (`docs/DEVELOPMENT.md` › Local Windows
|
||||
workstation). `npm run toolchain:check` compares any machine with the pins CI
|
||||
uses. The full Home Assistant harness cannot run on native Windows; without an
|
||||
importable `homeassistant` pytest does not collect `test_ha_*.py` at all, so a
|
||||
green pure run proves nothing about the harness (`docs/TESTING.md`). Cloud
|
||||
agents have the harness at `.venv-backend/bin/python`.
|
||||
|
||||
+30
-19
@@ -58,6 +58,22 @@ set; maintain the existing English and Russian documentation according to the
|
||||
project's normal rules. Right-to-left layout is a separate product project,
|
||||
because the plan canvas and editors cannot be mirrored by translations alone.
|
||||
|
||||
## Documentation screenshots
|
||||
|
||||
The images under `docs/images/` are produced only from synthetic data on the
|
||||
pinned Chromium in CI. A task branch does not commit them, nor the golden
|
||||
baselines: once per beta the `beta-derived.yml` workflow refreshes the
|
||||
fingerprint, the frames and the golden baselines on `dev` in one bot commit,
|
||||
accepting only the frames it was told to expect (`PROCESS.md` §8, #697). A task
|
||||
that changes visuals on purpose sets the `ci:golden` label. The manual path —
|
||||
the `Docs screenshots` workflow and `npm run docs:accept -- --reviewed
|
||||
--from=<unpacked artifact>`, or `--identical` when no pixel can move — stays
|
||||
for the release manager. Scenario version, source fingerprint and every image hash
|
||||
are recorded in the [screenshot index](docs/images/screenshots.json), and
|
||||
`node scripts/check-docs.mjs` reports a stale fingerprint: a warning on an
|
||||
ordinary push, an error on a beta candidate (a commit with a `Release:`
|
||||
trailer). The full rule is in `PROCESS.md` §8.
|
||||
|
||||
## Where to ask
|
||||
|
||||
Not sure whether something is a bug, or just want to discuss an idea before
|
||||
@@ -78,26 +94,22 @@ issue, but they do not replace it or maintain a separate checklist.
|
||||
|
||||
```bash
|
||||
git clone --filter=blob:none https://github.com/Matysh/houseplan-card && cd houseplan-card
|
||||
npm ci # frontend toolchain
|
||||
npm ci # frontend toolchain; `prepare` installs .githooks
|
||||
npm run typecheck # tsc --noEmit (strict)
|
||||
npm test # node:test — pure logic, i18n parity, tap-action security
|
||||
npm run build # tsc + rollup → dist/houseplan-card.js
|
||||
pip install pytest voluptuous && python -m pytest tests_backend -q # pure backend tests
|
||||
npm install # also installs .githooks through the prepare script
|
||||
pip install -r tests_backend/requirements.txt && python -m pytest tests_backend -q # CI pins, HA harness included
|
||||
```
|
||||
|
||||
### Why `--filter=blob:none` (#345)
|
||||
|
||||
A full clone is **215 MB of `.git`**; a blobless one is **26 MB** — measured, not
|
||||
estimated. Both carry all 1611 commits and all 182 tags, so ranges, `merge-base`
|
||||
and `git diff` across history work identically; `git diff origin/dev~3..origin/dev`
|
||||
in a blobless clone takes about a second and grows `.git` by one megabyte.
|
||||
|
||||
The difference is that historical *file contents* are fetched only if something
|
||||
actually asks for them. That matters here because 32% of the pack is documentation
|
||||
screenshots — ten PNGs re-captured 196 times — and another sizeable share is the
|
||||
committed bundle, one 1.16 MB file per product change. Almost nobody ever reads an
|
||||
old revision of either.
|
||||
A blobless clone keeps every commit and tag, so ranges, `merge-base` and
|
||||
`git diff` across history work exactly as in a full clone; only historical *file
|
||||
contents* are fetched on demand. Most of the pack is exactly such content that
|
||||
almost nobody reads again — documentation screenshots re-captured with the UI and
|
||||
the committed bundle rewritten by every release candidate — so a blobless clone
|
||||
is several times smaller. To see the numbers for your own clone, compare
|
||||
`git count-objects -vH` in a blobless and in a full one.
|
||||
|
||||
Drop the flag if you work offline with history, or need `git log -p` over the whole
|
||||
tree repeatedly. Do **not** replace it with `--depth=1`: a shallow clone is about
|
||||
@@ -114,7 +126,9 @@ Linux CI or WSL (`bash scripts/wsl-setup.sh --verify`). Without an importable
|
||||
## Ground rules
|
||||
|
||||
- **Docs in the same commit**: CHANGELOG entry for user-visible changes;
|
||||
`docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new gotchas.
|
||||
`docs/ARCHITECTURE.md` for changes to the data model, WS API or coordinate
|
||||
system; `docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new
|
||||
gotchas. A docs-only commit needs no trailers (`PROCESS.md` §3 п.10).
|
||||
- Every UI string goes through `src/i18n/<lang>.json`; follow the
|
||||
[Translations](#translations) flow for registry and backend parity.
|
||||
- The committed bundle changes only in a release candidate: `npm run
|
||||
@@ -130,8 +144,5 @@ Linux CI or WSL (`bash scripts/wsl-setup.sh --verify`). Without an importable
|
||||
## Architecture
|
||||
|
||||
Start with `docs/ARCHITECTURE.md` (data model, WS API, coordinate system) and
|
||||
`docs/STATUS.md` (current state). Release mechanics live in `docs/STATUS.md` › Workflow: the version sources are
|
||||
the ones checked by `scripts/release-contract.mjs`, prereleases go through
|
||||
`npm run release:prerelease -- <tag> --issues=… --yes` (or the manual
|
||||
`Publish prerelease` workflow), and stable installable assets are published only
|
||||
by `release.yml` (#540).
|
||||
`docs/STATUS.md` (current state). Release mechanics live in one place:
|
||||
`docs/DEVELOPMENT.md` › Release.
|
||||
|
||||
+784
-274
File diff suppressed because it is too large
Load Diff
@@ -189,6 +189,8 @@ concurrent clients, but avoid editing the same object in two browsers at once.
|
||||
- [Stairs and floor links](docs/STAIRS.md)
|
||||
- [Background editor](docs/DECOR-EDITOR.md)
|
||||
- [Robot vacuums](docs/VACUUM.md)
|
||||
- [Presence radars](docs/RADAR.md)
|
||||
- [PDF export](docs/PDF-EXPORT.md)
|
||||
- [Touch support](docs/TOUCH-SUPPORT.md)
|
||||
|
||||
<!-- docs-section: support -->
|
||||
@@ -201,9 +203,4 @@ concurrent clients, but avoid editing the same object in two browsers at once.
|
||||
Include the version, browser, logs and reproduction steps; private entity IDs
|
||||
may be replaced with fictional ones.
|
||||
|
||||
Documentation screenshots are produced by the reproducible
|
||||
`npm run build && node demo/docs/capture.mjs` command using synthetic data only. Scenario version,
|
||||
source fingerprint and every image hash are recorded in the
|
||||
[screenshot index](docs/images/screenshots.json).
|
||||
|
||||
License: [MIT](LICENSE).
|
||||
|
||||
+9
-7
@@ -43,6 +43,9 @@ Assistant — и устройства появятся на плане авто
|
||||
а карточки комнат показывают температуру, влажность, свет и средний LQI.
|
||||
- **Свет и окружение.** Заливки комнат, Glow от ламп, тени от стен, дневной фон и
|
||||
солнечные лучи из окон.
|
||||
- **Плоский план или 2.5D.** Один переключатель в «Общие настройки ›
|
||||
Отображение › Объёмный вид плана (2.5D)» показывает план с глубиной везде: приподнятые плитки устройств с мягкими
|
||||
тенями и мягкая засветка солнцем; декор и цвета стен остаются вашими.
|
||||
- **Двери, окна, ворота и пылесосы.** Проёмы отражают реальные датчики и замки;
|
||||
робот показывает позицию, базу и пройденный путь.
|
||||
- **Несколько этажей и экранов.** Вкладки пространств, жесты переключения,
|
||||
@@ -57,8 +60,9 @@ Assistant — и устройства появятся на плане авто
|
||||
## Первая рабочая комната
|
||||
|
||||
1. Установите интеграцию и откройте **House Plan** в боковом меню Home Assistant.
|
||||
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP либо выберите
|
||||
вариант без изображения, чтобы нарисовать план вручную.
|
||||
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP, возьмите уже
|
||||
загруженное изображение либо выберите вариант без изображения, чтобы
|
||||
нарисовать план вручную.
|
||||
3. В редакторе «План» выберите **Стены** и нарисуйте непрерывную цепочку по
|
||||
периметру комнаты: когда она замкнёт область, откроется диалог комнаты.
|
||||
4. Назовите комнату и свяжите её с зоной Home Assistant. Для помещения без
|
||||
@@ -188,8 +192,11 @@ default_floor: ground
|
||||
- [Полное руководство пользователя](docs/USER-GUIDE.ru.md)
|
||||
- [Матрица mouse/touch/keyboard](docs/USER-GUIDE.ru.md#6-навигация-масштаб-и-жесты)
|
||||
- [Инструменты плана](docs/USER-GUIDE.ru.md#инструменты-плана-в-короткой-таблице)
|
||||
- [Лестницы и переходы между этажами](docs/STAIRS.md)
|
||||
- [Редактор подложки](docs/DECOR-EDITOR.md)
|
||||
- [Роботы-пылесосы](docs/VACUUM.md)
|
||||
- [Радары присутствия](docs/RADAR.md)
|
||||
- [Экспорт в PDF](docs/PDF-EXPORT.md)
|
||||
- [Поддержка touch](docs/TOUCH-SUPPORT.md)
|
||||
|
||||
<!-- docs-section: support -->
|
||||
@@ -202,9 +209,4 @@ default_floor: ground
|
||||
обновление страницы (`Ctrl+F5`). Приложите версию, браузер, логи и шаги
|
||||
воспроизведения; приватные entity ID можно заменить вымышленными.
|
||||
|
||||
Скриншоты в документации получены воспроизводимой командой
|
||||
`npm run build && node demo/docs/capture.mjs` только на синтетических данных. Версия сценариев,
|
||||
fingerprint исходников и хеш каждого изображения находятся в
|
||||
[индексе снимков](docs/images/screenshots.json).
|
||||
|
||||
Лицензия: [MIT](LICENSE).
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 JB (justbusiness)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,47 @@
|
||||
# House Plan moon pack 1.0.0
|
||||
|
||||
Designer asset for [issue #661](https://github.com/Matysh/houseplan-card/issues/661)
|
||||
(moon in its current phase on the "Follow the Sun" background), variant A:
|
||||
one full-moon image, the phase is applied by the card as an SVG mask (C5).
|
||||
|
||||
- `moon.svg`: square `viewBox="0 0 512 512"`, disc centred at (256, 256),
|
||||
diameter 480 (16 px margin for anti-aliasing). Full moon, front-lit: no
|
||||
terminator, no shadow, no halo or glow outside the disc, no baked phase.
|
||||
Near side, north up, Mare Crisium on the right; the art is never mirrored.
|
||||
Owner 2026-09-29: the lit side is always the left one (as in the designer's
|
||||
phase sequence), waning runs the same states backwards; only the mask flips.
|
||||
Owner 2026-09-30: a distinct state for every day of the lunar month; the mask
|
||||
is continuous, so the card simply uses the daily illumination k.
|
||||
The dark side is not drawn: the card renders the same art at 8 % opacity.
|
||||
- Edge: one radial gradient ring with no hard outer edge (as in the designer's
|
||||
Figma export): bright rim 0.855-0.90 R, dark outline `#2b3138` at 50 % from
|
||||
0.905 R to 0.92 R (owner 2026-09-29, for a forced white background), then a
|
||||
1.5-unit fade to transparent by 0.935 R (r 239.4 in the 512 box, inside the 480
|
||||
disc). The soft limb hides any missing anti-aliasing of the outer edge.
|
||||
- The disc is masked (`<mask>`), not clipped: Chrome with GPU rasterisation draws
|
||||
`clip-path` edges without anti-aliasing (seen on Windows), masks are anti-aliased.
|
||||
- Art by JB (Figma, `House-plan` file). The Figma export (`source/figma-export/Full.svg`
|
||||
in the source archive) was normalised for the pack by `source/convert_jb.py`:
|
||||
viewBox 200 -> 512 (scale 2.56, disc clipped at r 240), the `mix-blend-mode`
|
||||
screen ring and multiply groups removed (blend modes depend on the stacking
|
||||
context they land in, so the file now composites normally and looks the same
|
||||
everywhere), the three 5-7 % crater layers and the 484-path speckle texture
|
||||
dropped (invisible at 200 px), and each mare "blend" of 22 nested rings
|
||||
thinned to 4 rings (steps 0, 8, 16, 21). The export's screen-blended edge ring is rebuilt
|
||||
as a normal-composited radial gradient (the art is masked at r 230), so the feathered edge is part
|
||||
of the 480 disc and nothing is drawn outside it. Path coordinates rounded to 0.1, gradient offsets kept at 0.001 (`svgo.config.mjs`).
|
||||
- Vector only: two `radialGradient`s (disc, edge), one `mask`, paths. No `filter`,
|
||||
`feGaussianBlur`, `feImage`, embedded raster, fonts or external references.
|
||||
25.6 KB raw, about 9 KB gzip.
|
||||
- Colours: disc `#949493` (centre) -> `#D6D5D4` (rim), maria grey rings
|
||||
`#404040 -> white` under a 0.2-0.6 group opacity. Checked on the night
|
||||
(`#111a27 -> #1f2f3e`), dusk (`#48536c -> #9a7380`) and dawn
|
||||
(`#aabdd1 -> #e8c8b7`) palettes at 200, 120 and 94 px.
|
||||
- `pack.json`: pack id, version, licence, source link and the disc geometry
|
||||
for `scripts/generate-moon-assets.mjs`.
|
||||
|
||||
JB (justbusiness) supplies this asset for House Plan to use, modify and
|
||||
distribute under the repository MIT License without separate UI attribution
|
||||
(`LICENSE.md`). The reviewed source archive with its SHA-256 is attached to
|
||||
issue #661. Generated TypeScript (`src/moon-art.generated.ts`) must not be
|
||||
edited by hand; regenerate it from this pack.
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pack_id": "houseplan",
|
||||
"pack_version": "1.0.0",
|
||||
"title_ru": "Луна: полный диск для маски фазы",
|
||||
"title_en": "Moon: full disc for the phase mask",
|
||||
"author": "JB (justbusiness)",
|
||||
"license": "MIT",
|
||||
"source_url": "https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan",
|
||||
"issue": 661,
|
||||
"file": "moon.svg",
|
||||
"view_box": [
|
||||
0,
|
||||
0,
|
||||
512,
|
||||
512
|
||||
],
|
||||
"disc": {
|
||||
"cx": 256,
|
||||
"cy": 256,
|
||||
"r": 240
|
||||
},
|
||||
"lit": "front",
|
||||
"north_up": true,
|
||||
"east_right": true,
|
||||
"notes_en": "Full moon, front-lit, no terminator, no shadow, no glow outside the disc; thin dark outline for light backgrounds. Art by JB (Figma export normalised: viewBox 512, blend modes removed, rings thinned). Phase mask and the 8 % dark side are applied by the card (C5). The art is never mirrored; the lit side is always the left one, waning runs the same states backwards (owner 2026-09-29).",
|
||||
"notes_ru": "Полная луна, освещение в лоб, без терминатора, тени и ореола; тонкая тёмная обводка для светлого фона. Арт JB (экспорт из Figma нормализован: viewBox 512, без blend-режимов, кольца морей прорежены). Маску фазы и тёмную сторону 8 % накладывает карточка (C5). Арт не зеркалится; освещённая сторона всегда слева, убывание идёт теми же состояниями в обратном порядке (владелец 29.09).",
|
||||
"mirror": false,
|
||||
"outline": {
|
||||
"color": "#2b3138",
|
||||
"opacity": 0.5,
|
||||
"from_r": 0.905,
|
||||
"to_r": 0.92,
|
||||
"fade_to_r": 0.935,
|
||||
"note": "part of the edge gradient, no separate stroke"
|
||||
},
|
||||
"lit_side": "left"
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -39,6 +40,34 @@ from .store import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_flush_runtime(
|
||||
hass: HomeAssistant, entry: HouseplanConfigEntry,
|
||||
) -> None:
|
||||
"""Stop deferred writers and persist their latest state once.
|
||||
|
||||
Home Assistant stop and config-entry unload share this exact path. Both
|
||||
writers are independently guarded so one failed store cannot prevent the
|
||||
other from reaching disk during shutdown.
|
||||
"""
|
||||
zigbee = getattr(entry.runtime_data, "zigbee_coordinator", None)
|
||||
if zigbee is not None:
|
||||
await zigbee.async_close()
|
||||
|
||||
recorder = hass.data.get(DOMAIN, {}).get("trail_recorder")
|
||||
if recorder is not None:
|
||||
try:
|
||||
await recorder.async_teardown()
|
||||
except Exception: # noqa: BLE001 - shutdown must continue with other stores
|
||||
_LOGGER.exception("House Plan: flushing vacuum trails failed")
|
||||
|
||||
virtual_lights = getattr(entry.runtime_data, "virtual_lights", None)
|
||||
if virtual_lights is not None:
|
||||
try:
|
||||
await virtual_lights.async_flush()
|
||||
except Exception: # noqa: BLE001 - shutdown must continue with other stores
|
||||
_LOGGER.exception("House Plan: flushing virtual-light state failed")
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config) -> bool:
|
||||
"""Register global handlers (survive config-entry reloads): WS commands, HTTP view."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
@@ -86,6 +115,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
|
||||
_LOGGER.exception("House Plan: virtual-light storage is not readable; using default on")
|
||||
entry.runtime_data = data
|
||||
|
||||
from .zigbee_topology import ZigbeeScanCoordinator
|
||||
data.zigbee_coordinator = ZigbeeScanCoordinator(hass, entry)
|
||||
|
||||
from .radar import RadarCoordinator
|
||||
data.radar_coordinator = RadarCoordinator(hass, data)
|
||||
await data.radar_coordinator.async_setup()
|
||||
@@ -252,6 +284,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
|
||||
# sidebar entry after a migration, repair or initial housekeeping failure.
|
||||
panel_path = Path(__file__).parent / "frontend" / "houseplan-panel.js"
|
||||
await async_setup_panel_registration(hass, entry, panel_path)
|
||||
|
||||
async def _flush_on_stop(_event) -> None:
|
||||
stop_listener[0] = None
|
||||
await _async_flush_runtime(hass, entry)
|
||||
|
||||
stop_listener = [hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _flush_on_stop)]
|
||||
|
||||
def _remove_stop_listener() -> None:
|
||||
if stop_listener[0] is not None:
|
||||
stop_listener[0]()
|
||||
stop_listener[0] = None
|
||||
|
||||
entry.async_on_unload(_remove_stop_listener)
|
||||
return True
|
||||
|
||||
|
||||
@@ -263,12 +308,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) -
|
||||
entry is loaded. Static paths cannot be unregistered by design.
|
||||
"""
|
||||
remove_panel_registration(hass)
|
||||
rec = hass.data.get(DOMAIN, {}).pop("trail_recorder", None)
|
||||
if rec:
|
||||
await rec.async_teardown()
|
||||
virtual_lights = getattr(entry.runtime_data, "virtual_lights", None)
|
||||
if virtual_lights is not None:
|
||||
await virtual_lights.async_flush()
|
||||
await _async_flush_runtime(hass, entry)
|
||||
hass.data.get(DOMAIN, {}).pop("trail_recorder", None)
|
||||
if entry.runtime_data.radar_coordinator:
|
||||
entry.runtime_data.radar_coordinator.teardown()
|
||||
entry.runtime_data.radar_coordinator = None
|
||||
|
||||
@@ -57,7 +57,7 @@ SCHEDULED_GRACE_S = 30 * 24 * 3600
|
||||
FILES_DIR = "houseplan/files"
|
||||
ASSETS_DIR = "houseplan/assets"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.78.0-beta.5"
|
||||
VERSION = "1.80.0-beta.4"
|
||||
DECOR_ASSETS_API_VERSION = 1
|
||||
SUMMARY_PANEL_API_VERSION = 1
|
||||
|
||||
@@ -91,7 +91,9 @@ MAX_IMPORT_PREVIEWS_TOTAL = 3
|
||||
DEFAULT_CONFIG: dict[str, object] = {
|
||||
"spaces": [],
|
||||
"markers": [],
|
||||
"settings": {"bg_mode": "daynight", "sun_ray_origin": "inner"},
|
||||
# #661: new installations get the moon; an existing config without the key
|
||||
# keeps its look (docs/SUN.md: an update never changes a stored plan).
|
||||
"settings": {"bg_mode": "daynight", "sun_ray_origin": "inner", "moon": True},
|
||||
}
|
||||
|
||||
# #42: THE stable public error-code contract. Every code a user-facing
|
||||
|
||||
@@ -86,6 +86,16 @@ def _lattice_points(value: Any) -> None:
|
||||
_lattice_point(point)
|
||||
|
||||
|
||||
def _scalar_points(value: Any) -> None:
|
||||
if not isinstance(value, list):
|
||||
return
|
||||
for point in value:
|
||||
if not isinstance(point, list):
|
||||
continue
|
||||
for index in range(min(2, len(point))):
|
||||
point[index] = canonicalize_number(point[index])
|
||||
|
||||
|
||||
def canonicalize_position(position: Any) -> Any:
|
||||
"""Canonicalise lattice x/y in one layout record, preserving metadata."""
|
||||
result = copy.deepcopy(position)
|
||||
@@ -182,6 +192,12 @@ def canonicalize_config_geometry(config: Any) -> Any:
|
||||
_lattice_point(span.get("a"))
|
||||
_lattice_point(span.get("b"))
|
||||
|
||||
for strip in _records(space.get("led_strips")):
|
||||
# #780: LED strips snap to physical wall faces, like stairs and
|
||||
# furniture: remove JSON noise only, never pull a face contact
|
||||
# onto a lattice node.
|
||||
_scalar_points(strip.get("points"))
|
||||
|
||||
for marker in _records(root.get("markers")):
|
||||
_scalar_fields(marker, ("angle",))
|
||||
|
||||
|
||||
@@ -1,337 +1,500 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96",
|
||||
"fingerprint": "06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698",
|
||||
"entry": "houseplan-card.js",
|
||||
"panelEntry": "houseplan-panel.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 299093,
|
||||
"initialViewGzipBytes": 301044,
|
||||
"initialPanelFiles": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-panel.js"
|
||||
],
|
||||
"initialPanelGzipBytes": 300614,
|
||||
"initialPanelGzipBytes": 302566,
|
||||
"initialPanelOnlyFiles": [
|
||||
"houseplan-panel.js"
|
||||
],
|
||||
"initialPanelOnlyGzipBytes": 2324,
|
||||
"initialPanelOnlyGzipBytes": 2326,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"houseplan-assets/de-B2abA_bU.js",
|
||||
"houseplan-assets/editor-BLL8Ue28.js",
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/fr-Bho0FfMD.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-FEM5fqpW.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-CiQlpis3.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-BeNZqp-_.js",
|
||||
"houseplan-assets/iso-scene-render-Soh6e8O4.js",
|
||||
"houseplan-assets/live-interaction-runtime-D6A_AoBE.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"houseplan-assets/pdf-export-De59asII.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-jjP-5y3_.js",
|
||||
"houseplan-assets/zigbee-topology-BbetEPvj.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-sjaKOGGU.js"
|
||||
"houseplan-assets/backdrop-pick-BlKYl_U7.js",
|
||||
"houseplan-assets/de-7Zku19fP.js",
|
||||
"houseplan-assets/editor-CmIdArPY.js",
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/fr-CmN_ktKN.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-2dyTGznE.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D3GuUom3.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-TWYzHq8F.js",
|
||||
"houseplan-assets/iso-scene-render-3D4T2rIf.js",
|
||||
"houseplan-assets/led-strip-editor-BV28Sv6H.js",
|
||||
"houseplan-assets/led-strip-geometry-DCOEb7ps.js",
|
||||
"houseplan-assets/led-strip-runtime-C3jpBGK_.js",
|
||||
"houseplan-assets/live-interaction-runtime-BI3F6BM_.js",
|
||||
"houseplan-assets/live-tip-placement-DJeWazgc.js",
|
||||
"houseplan-assets/moon-runtime-BOGkZE2h.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js",
|
||||
"houseplan-assets/pdf-export-C6l2UAvp.js",
|
||||
"houseplan-assets/space-editor-BItHjX5t.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-l2frpvJL.js",
|
||||
"houseplan-assets/zigbee-topology-DtQtLxJ3.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-9_MBQQpH.js"
|
||||
],
|
||||
"lazyGzipBytes": 453435,
|
||||
"lazyGzipBytes": 485800,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"houseplan-assets/editor-BLL8Ue28.js",
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-FEM5fqpW.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"houseplan-assets/zigbee-topology-BbetEPvj.js"
|
||||
"houseplan-assets/backdrop-pick-BlKYl_U7.js",
|
||||
"houseplan-assets/editor-CmIdArPY.js",
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-2dyTGznE.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js",
|
||||
"houseplan-assets/zigbee-topology-DtQtLxJ3.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 235435,
|
||||
"lazyEditorGzipBytes": 243791,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-CiQlpis3.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js"
|
||||
"houseplan-assets/backdrop-pick-BlKYl_U7.js",
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D3GuUom3.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 27952,
|
||||
"lazyOnboardingGzipBytes": 28672,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-B2abA_bU.js",
|
||||
"houseplan-assets/fr-Bho0FfMD.js"
|
||||
"houseplan-assets/de-7Zku19fP.js",
|
||||
"houseplan-assets/fr-CmN_ktKN.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 56230,
|
||||
"lazyLocaleGzipBytes": 53862,
|
||||
"lazyIsometricFiles": [
|
||||
"houseplan-assets/iso-scene-render-Soh6e8O4.js"
|
||||
"houseplan-assets/iso-scene-render-3D4T2rIf.js"
|
||||
],
|
||||
"lazyIsometricGzipBytes": 17885,
|
||||
"lazyIsometricGzipBytes": 12347,
|
||||
"lazyFurnitureArtFiles": [
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js"
|
||||
],
|
||||
"lazyFurnitureArtGzipBytes": 16941,
|
||||
"lazyFurnitureArtGzipBytes": 16939,
|
||||
"lazyPdfFiles": [
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/pdf-export-De59asII.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"houseplan-assets/pdf-export-C6l2UAvp.js"
|
||||
],
|
||||
"lazyPdfGzipBytes": 126914,
|
||||
"lazyPdfGzipBytes": 126936,
|
||||
"lazyMoonFiles": [
|
||||
"houseplan-assets/moon-runtime-BOGkZE2h.js"
|
||||
],
|
||||
"lazyMoonGzipBytes": 11715,
|
||||
"lazyLedFiles": [
|
||||
"houseplan-assets/led-strip-field-aqwdQqPw.js",
|
||||
"houseplan-assets/led-strip-geometry-DCOEb7ps.js",
|
||||
"houseplan-assets/led-strip-runtime-C3jpBGK_.js"
|
||||
],
|
||||
"lazyLedGzipBytes": 10686,
|
||||
"lazyLedEditorFiles": [
|
||||
"houseplan-assets/led-strip-editor-BV28Sv6H.js",
|
||||
"houseplan-assets/led-strip-geometry-DCOEb7ps.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js"
|
||||
],
|
||||
"lazyLedEditorGzipBytes": 10704,
|
||||
"lazyNamespaceLocaleFiles": [
|
||||
"houseplan-assets/settings-de-BjfMuSw8.js",
|
||||
"houseplan-assets/settings-fr-BKCX3R6z.js",
|
||||
"houseplan-assets/settings-ru-D8xvcCw6.js",
|
||||
"houseplan-assets/support-de-C9Oo6umO.js",
|
||||
"houseplan-assets/support-fr-CRCSYq8q.js",
|
||||
"houseplan-assets/support-ru-BcRlKfAd.js",
|
||||
"houseplan-assets/topology-de-BjBB8qiK.js",
|
||||
"houseplan-assets/topology-fr-DXFxoYd-.js",
|
||||
"houseplan-assets/topology-ru-Di7X71yN.js"
|
||||
"houseplan-assets/led-de-DrXCZ2sy.js",
|
||||
"houseplan-assets/led-fr-Dpe8-hj5.js",
|
||||
"houseplan-assets/led-ru-Bn4KLybY.js",
|
||||
"houseplan-assets/settings-de-NUJhD35v.js",
|
||||
"houseplan-assets/settings-fr-BxWFQjqM.js",
|
||||
"houseplan-assets/settings-ru-DxtPqpRy.js",
|
||||
"houseplan-assets/support-de-B189S_5G.js",
|
||||
"houseplan-assets/support-fr-BO-hSBLk.js",
|
||||
"houseplan-assets/support-ru-DslL3KcH.js",
|
||||
"houseplan-assets/tools-de-DaCueVOA.js",
|
||||
"houseplan-assets/tools-fr-rOKrLY3t.js",
|
||||
"houseplan-assets/tools-ru-qWSdpJow.js",
|
||||
"houseplan-assets/topology-de-wNwfkZjf.js",
|
||||
"houseplan-assets/topology-fr-D9pmNHVr.js",
|
||||
"houseplan-assets/topology-ru-D6m0B05V.js"
|
||||
],
|
||||
"lazyNamespaceLocaleGzipBytes": 21047,
|
||||
"lazyNamespaceLocaleGzipBytes": 31734,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"sha256": "001e8011f17ee8c1d3804304eb6734065b645a28b19beb8c18fa699f309de66c",
|
||||
"rawBytes": 54017,
|
||||
"gzipBytes": 17577,
|
||||
"path": "houseplan-assets/backdrop-pick-BlKYl_U7.js",
|
||||
"sha256": "6b955d4d79600a2ea568047ed158afc056fdafda5d5fea3fa6b6d3f4c7e336fc",
|
||||
"rawBytes": 54425,
|
||||
"gzipBytes": 17699,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js"
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/settings-de-BjfMuSw8.js",
|
||||
"houseplan-assets/settings-fr-BKCX3R6z.js",
|
||||
"houseplan-assets/settings-ru-D8xvcCw6.js"
|
||||
"houseplan-assets/settings-de-NUJhD35v.js",
|
||||
"houseplan-assets/settings-fr-BxWFQjqM.js",
|
||||
"houseplan-assets/settings-ru-DxtPqpRy.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-B2abA_bU.js",
|
||||
"sha256": "63b70f53f23e194e16172c10b11dda1d19db0bebc23a5213444cab07da1e72be",
|
||||
"rawBytes": 93993,
|
||||
"gzipBytes": 28342,
|
||||
"path": "houseplan-assets/de-7Zku19fP.js",
|
||||
"sha256": "fc6989b8d40ada5fa07cc3e7e96b7869e1ce1b8518dbd75319ffbfef2c241db4",
|
||||
"rawBytes": 89873,
|
||||
"gzipBytes": 27163,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-BLL8Ue28.js",
|
||||
"sha256": "6176410d822670169447e2324d7af4ffc7f500e914fd597b0318c41785adb31c",
|
||||
"path": "houseplan-assets/editor-CmIdArPY.js",
|
||||
"sha256": "f0ddb6c0402f2a4cd7160f5cf06bf424f57bc831031e95d8defa4310c0149b44",
|
||||
"rawBytes": 3833,
|
||||
"gzipBytes": 1581,
|
||||
"gzipBytes": 1585,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"sha256": "e45b8023f44f1048304d5d43616f51ea797f9a3475a801f776f9c488b93a1435",
|
||||
"rawBytes": 25600,
|
||||
"gzipBytes": 5056,
|
||||
"path": "houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"sha256": "036f5149e71d886f8707d7b8a6335a4222e564ea5b260d5cfda5e2e13729c040",
|
||||
"rawBytes": 1104,
|
||||
"gzipBytes": 578,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-Bho0FfMD.js",
|
||||
"sha256": "ae1939c095197487f36b8d34e06f976ac8e665f3542d9dc9cf06190963310ac3",
|
||||
"rawBytes": 96369,
|
||||
"gzipBytes": 27888,
|
||||
"path": "houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"sha256": "6dd7fe7e624218dfe80e0c02df0a555f509b6bee7c8855f644ced209682a4e46",
|
||||
"rawBytes": 25605,
|
||||
"gzipBytes": 5058,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"sha256": "ae4b691e03b0869b927ab76dfce816e0e50662bb9c78de68d2a66cdd2ead163e",
|
||||
"path": "houseplan-assets/fr-CmN_ktKN.js",
|
||||
"sha256": "00270b63c9403ccc23c216d1f12c55d3f552db97afc001892e3bf7c914737c4a",
|
||||
"rawBytes": 92266,
|
||||
"gzipBytes": 26699,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"sha256": "2b7ab5e089eb0d16b27f3963e56a7cb60693f4462594fe88e6cb430dc9eaec31",
|
||||
"rawBytes": 52035,
|
||||
"gzipBytes": 16941,
|
||||
"gzipBytes": 16939,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"sha256": "8746be63df29372816cf31f039e60165f1c7906565ab25cef8de52937cd88e89",
|
||||
"rawBytes": 1055297,
|
||||
"gzipBytes": 298290,
|
||||
"path": "houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"sha256": "3b1c90b7c98aa0e02bbeff529e4434a216b1a3a071f0da2d2a610800e0ecf1c6",
|
||||
"rawBytes": 1059200,
|
||||
"gzipBytes": 300240,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-B2abA_bU.js",
|
||||
"houseplan-assets/editor-BLL8Ue28.js",
|
||||
"houseplan-assets/fr-Bho0FfMD.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-FEM5fqpW.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-CiQlpis3.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-BeNZqp-_.js",
|
||||
"houseplan-assets/iso-scene-render-Soh6e8O4.js",
|
||||
"houseplan-assets/live-interaction-runtime-D6A_AoBE.js",
|
||||
"houseplan-assets/pdf-export-De59asII.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-jjP-5y3_.js"
|
||||
"houseplan-assets/de-7Zku19fP.js",
|
||||
"houseplan-assets/editor-CmIdArPY.js",
|
||||
"houseplan-assets/fr-CmN_ktKN.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-2dyTGznE.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-D3GuUom3.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-TWYzHq8F.js",
|
||||
"houseplan-assets/iso-scene-render-3D4T2rIf.js",
|
||||
"houseplan-assets/led-strip-editor-BV28Sv6H.js",
|
||||
"houseplan-assets/led-strip-runtime-C3jpBGK_.js",
|
||||
"houseplan-assets/live-interaction-runtime-BI3F6BM_.js",
|
||||
"houseplan-assets/moon-runtime-BOGkZE2h.js",
|
||||
"houseplan-assets/pdf-export-C6l2UAvp.js",
|
||||
"houseplan-assets/space-editor-BItHjX5t.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-l2frpvJL.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-FEM5fqpW.js",
|
||||
"sha256": "1a2c3ca19fd2ecdfe8e4fbbe2b0d187d44dc986cf97a7d5d13a47d0edf2baad0",
|
||||
"rawBytes": 679701,
|
||||
"gzipBytes": 189570,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-2dyTGznE.js",
|
||||
"sha256": "7e71fabc225c825694c7f1dca1ab89859ae7d1f8481f62a01d11d971b5486a86",
|
||||
"rawBytes": 698926,
|
||||
"gzipBytes": 195573,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"houseplan-assets/zigbee-topology-BbetEPvj.js"
|
||||
"houseplan-assets/backdrop-pick-BlKYl_U7.js",
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js",
|
||||
"houseplan-assets/zigbee-topology-DtQtLxJ3.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/support-de-C9Oo6umO.js",
|
||||
"houseplan-assets/support-fr-CRCSYq8q.js",
|
||||
"houseplan-assets/support-ru-BcRlKfAd.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-sjaKOGGU.js"
|
||||
"houseplan-assets/support-de-B189S_5G.js",
|
||||
"houseplan-assets/support-fr-BO-hSBLk.js",
|
||||
"houseplan-assets/support-ru-DslL3KcH.js",
|
||||
"houseplan-assets/tools-de-DaCueVOA.js",
|
||||
"houseplan-assets/tools-fr-rOKrLY3t.js",
|
||||
"houseplan-assets/tools-ru-qWSdpJow.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-9_MBQQpH.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-CiQlpis3.js",
|
||||
"sha256": "08a26f899d0a5f8f71a2ed6f71d095388aa9eead14ce4a4593ce21ee2e54ea14",
|
||||
"rawBytes": 15886,
|
||||
"gzipBytes": 4910,
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-D3GuUom3.js",
|
||||
"sha256": "d77f02c0917154f5c1fefc63ad7445b6260fbbd3ec2ac4738e22777c75dc5bf9",
|
||||
"rawBytes": 15930,
|
||||
"gzipBytes": 4930,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js"
|
||||
"houseplan-assets/backdrop-pick-BlKYl_U7.js",
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/hp-zigbee-topology-overlay-BeNZqp-_.js",
|
||||
"sha256": "5360f9165e07d274bc2e71634792f9d8868b20750f4565feeee98d48e9ac08fa",
|
||||
"rawBytes": 10958,
|
||||
"gzipBytes": 3764,
|
||||
"path": "houseplan-assets/hp-zigbee-topology-overlay-TWYzHq8F.js",
|
||||
"sha256": "9f9a41dc101f986e5579580b487fa443763ae84b983e7002557feb1d87477997",
|
||||
"rawBytes": 16715,
|
||||
"gzipBytes": 5524,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"houseplan-assets/zigbee-topology-BbetEPvj.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-sjaKOGGU.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/live-tip-placement-DJeWazgc.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js",
|
||||
"houseplan-assets/zigbee-topology-DtQtLxJ3.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-9_MBQQpH.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/iso-scene-render-Soh6e8O4.js",
|
||||
"sha256": "192b514d86778182905c9ade6b2f2f28e8428b281b0190ebe9b159d3170eaf98",
|
||||
"rawBytes": 52970,
|
||||
"gzipBytes": 17885,
|
||||
"path": "houseplan-assets/iso-scene-render-3D4T2rIf.js",
|
||||
"sha256": "b9afcc206e4008d23d18a588a43bb05b4929f51fcbe4d19411cacf09dd13c2d3",
|
||||
"rawBytes": 36700,
|
||||
"gzipBytes": 12347,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/live-interaction-runtime-D6A_AoBE.js",
|
||||
"sha256": "8a3cd740cd67f85041975cf18d9819b6d44be34601e56d639664dce2b79b9873",
|
||||
"rawBytes": 8216,
|
||||
"gzipBytes": 3207,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"sha256": "46a700cfd83daaaa2ddebe209e8395e08ae6c1e84f286e1f5807c31604e90130",
|
||||
"rawBytes": 675,
|
||||
"gzipBytes": 409,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/pdf-export-De59asII.js",
|
||||
"sha256": "4f1c72412bade0a94e89cc49fb44cce1b90c5304186506d0254fd0de25b26c31",
|
||||
"rawBytes": 236608,
|
||||
"gzipBytes": 109973,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-de-BjfMuSw8.js",
|
||||
"sha256": "62cda042ab64811c685f49f5aa338308e592f464f39afc1440de0031cdd97cf0",
|
||||
"rawBytes": 7378,
|
||||
"gzipBytes": 2878,
|
||||
"path": "houseplan-assets/led-de-DrXCZ2sy.js",
|
||||
"sha256": "6c289c749204ef31d1404170038deda2503fc2bcac407ad00cf9b3143feeb660",
|
||||
"rawBytes": 1907,
|
||||
"gzipBytes": 919,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-fr-BKCX3R6z.js",
|
||||
"sha256": "57246900bc095d6a84a2e7db9c96f7bb634aeb056957c431abbff760bed335db",
|
||||
"rawBytes": 7620,
|
||||
"gzipBytes": 2841,
|
||||
"path": "houseplan-assets/led-fr-Dpe8-hj5.js",
|
||||
"sha256": "97410cb19c4d915f7e74b0d7fe30a3c5560ff07cba6de06daf32731c852415eb",
|
||||
"rawBytes": 1886,
|
||||
"gzipBytes": 903,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-ru-D8xvcCw6.js",
|
||||
"sha256": "cf1089c5af8bffe24f153ae8553b8eeb14577862cb34b3ee51e2061b1585091c",
|
||||
"rawBytes": 10304,
|
||||
"gzipBytes": 3325,
|
||||
"path": "houseplan-assets/led-ru-Bn4KLybY.js",
|
||||
"sha256": "bef109c08ac7fdce7e9c7046bd699a524d6645ea1548e98c89cbb0e25e7867a0",
|
||||
"rawBytes": 2525,
|
||||
"gzipBytes": 1104,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-editor-CwTXBm1f.js",
|
||||
"sha256": "6ccad563f28ed17d527b6e2a6efc167b696f478c7c23b114bf904ac4e695f566",
|
||||
"path": "houseplan-assets/led-strip-editor-BV28Sv6H.js",
|
||||
"sha256": "3c57d810c1248e97f41e10cd54bf688c4954ec9fd63f323d8dc464bd21179889",
|
||||
"rawBytes": 21704,
|
||||
"gzipBytes": 7380,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/led-strip-geometry-DCOEb7ps.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/led-de-DrXCZ2sy.js",
|
||||
"houseplan-assets/led-fr-Dpe8-hj5.js",
|
||||
"houseplan-assets/led-ru-Bn4KLybY.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-field-aqwdQqPw.js",
|
||||
"sha256": "edc05f9c9778f7bf784797dbdc538109a6e5a2da131ca222b37b5716d3eb6f45",
|
||||
"rawBytes": 8066,
|
||||
"gzipBytes": 3380,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/led-strip-geometry-DCOEb7ps.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-geometry-DCOEb7ps.js",
|
||||
"sha256": "6ea10abf3d2ebba4e2ad88b8b7c8bd9b1448f8d425ac2a509efe5837acf66ec3",
|
||||
"rawBytes": 7055,
|
||||
"gzipBytes": 2917,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-runtime-C3jpBGK_.js",
|
||||
"sha256": "8098a7c8724a4af566e58019e69d99950b29ad43575b31ad0f87afca3a430b8d",
|
||||
"rawBytes": 11248,
|
||||
"gzipBytes": 4389,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/led-strip-geometry-DCOEb7ps.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/led-strip-field-aqwdQqPw.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/live-interaction-runtime-BI3F6BM_.js",
|
||||
"sha256": "0b61352b8dc2a78900f6a3029b65dba6d0ea07c1ede08db1ffa05426a31ad5ee",
|
||||
"rawBytes": 9686,
|
||||
"gzipBytes": 3774,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/floating-surface-pyD_i_Ly.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/live-tip-placement-DJeWazgc.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/live-tip-placement-DJeWazgc.js",
|
||||
"sha256": "307cc62e611a50e98ce322d26236f00b9a39e212018e81ce7b4a8d3ee1c0edab",
|
||||
"rawBytes": 1191,
|
||||
"gzipBytes": 681,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/moon-runtime-BOGkZE2h.js",
|
||||
"sha256": "2d8e1065f280249d2ef767708272932876d8d3416ea39a9b6735c53477c32307",
|
||||
"rawBytes": 30292,
|
||||
"gzipBytes": 11715,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/namespace-language-COmALraj.js",
|
||||
"sha256": "24db675a12ac3f446b676bc47e897b9c370a021c3b5226a9858b1c4207e39166",
|
||||
"rawBytes": 670,
|
||||
"gzipBytes": 407,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/pdf-export-C6l2UAvp.js",
|
||||
"sha256": "b575454475fc1df4c064a32d298405c456a4556269dbcb6acbd348c73f5c9e08",
|
||||
"rawBytes": 236714,
|
||||
"gzipBytes": 109997,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/furniture-plan-art.generated-D2WZwvLw.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-de-NUJhD35v.js",
|
||||
"sha256": "ef301ab4e061002a69c0b56235b4a72bad553e95191067c76b2e290443dd53e8",
|
||||
"rawBytes": 8886,
|
||||
"gzipBytes": 3407,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-fr-BxWFQjqM.js",
|
||||
"sha256": "5b6e67388b6ec9dcae763d6e6f00c68ced961f71159c132606d51eabddc27800",
|
||||
"rawBytes": 9297,
|
||||
"gzipBytes": 3381,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-ru-DxtPqpRy.js",
|
||||
"sha256": "c3bfb770748ae6ea309042c1308d8e0a3cd3e5a659eb0a9c9a1e322af273074b",
|
||||
"rawBytes": 12284,
|
||||
"gzipBytes": 3897,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-editor-BItHjX5t.js",
|
||||
"sha256": "e2afb8f0c686d2639641a5986b0b0777ba8a866484be820421dcc98d9b59c989",
|
||||
"rawBytes": 2619,
|
||||
"gzipBytes": 1129,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-editor-D8zcin5K.js",
|
||||
"sha256": "4544e4f4536440df602849a85577ca64615000d867eb8721f9d3b9773f81887a",
|
||||
"rawBytes": 14862,
|
||||
"gzipBytes": 3423,
|
||||
"gzipBytes": 3426,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-jjP-5y3_.js"
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-l2frpvJL.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-metrics-Bd81K-sc.js",
|
||||
"sha256": "20851cfa3b7d45452f558ac6ec231d6ba7a0a9b1459760580fa56ce7c5554a05",
|
||||
"path": "houseplan-assets/summary-panel-metrics-DMn7iDbG.js",
|
||||
"sha256": "10dc343eb95eee0d6d09fbf936e7771314d8a14f76eadea7dbc5d8fb80caf688",
|
||||
"rawBytes": 2701,
|
||||
"gzipBytes": 1344,
|
||||
"gzipBytes": 1345,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-runtime-loaded-jjP-5y3_.js",
|
||||
"sha256": "1ed46af8f90254099376b7107163e5ee6dfb282924409bd5a5fa40036660c2eb",
|
||||
"rawBytes": 72959,
|
||||
"gzipBytes": 20242,
|
||||
"path": "houseplan-assets/summary-panel-runtime-loaded-l2frpvJL.js",
|
||||
"sha256": "ea892ff2a96524e2bda9592343fb5896211ab67cd4cff4b6168464e9f753fac2",
|
||||
"rawBytes": 73476,
|
||||
"gzipBytes": 20487,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/form-kit.styles-DNNRm1qV.js",
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/summary-panel-editor-CwTXBm1f.js",
|
||||
"houseplan-assets/summary-panel-metrics-Bd81K-sc.js"
|
||||
"houseplan-assets/summary-panel-editor-D8zcin5K.js",
|
||||
"houseplan-assets/summary-panel-metrics-DMn7iDbG.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-de-C9Oo6umO.js",
|
||||
"sha256": "aeab7835cf28821f302612a778f0687b05cb006c5371bbfeed15976ac0aba3c1",
|
||||
"path": "houseplan-assets/support-de-B189S_5G.js",
|
||||
"sha256": "ec04fe04c616f01d3aedcc04dac23e05422c6a81dff7f9f637b76eb3493befd5",
|
||||
"rawBytes": 6167,
|
||||
"gzipBytes": 2396,
|
||||
"isEntry": false,
|
||||
@@ -339,98 +502,125 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-fr-CRCSYq8q.js",
|
||||
"sha256": "ff04381645035c564a6455ddfbfd81de57960e63977e4979ec1c04b4564f7885",
|
||||
"path": "houseplan-assets/support-fr-BO-hSBLk.js",
|
||||
"sha256": "608332d7e2dc32b71dbae536d7876dc51116c17195f234333c983cdb92f026ae",
|
||||
"rawBytes": 6333,
|
||||
"gzipBytes": 2429,
|
||||
"gzipBytes": 2431,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-ru-BcRlKfAd.js",
|
||||
"sha256": "0603d82d78e310ba8eaf26d4cc1b7ceeadf5cfabb40042199cb5b85b1089000c",
|
||||
"path": "houseplan-assets/support-ru-DslL3KcH.js",
|
||||
"sha256": "7bc2dc39a74c9fa30bb443c58db75adb427477eac6d3a5db47a90038a6bc1669",
|
||||
"rawBytes": 8376,
|
||||
"gzipBytes": 2875,
|
||||
"gzipBytes": 2876,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-de-BjBB8qiK.js",
|
||||
"sha256": "df225ea5dc6b1886537cd7a0cec055530773ba31c94714a9cc199fd6fae2beb4",
|
||||
"rawBytes": 2698,
|
||||
"gzipBytes": 1334,
|
||||
"path": "houseplan-assets/tools-de-DaCueVOA.js",
|
||||
"sha256": "da4aa9fa59fe352d6d1c9330c0700bbb849e77522ada7783f6e29a114b7b3033",
|
||||
"rawBytes": 4130,
|
||||
"gzipBytes": 1375,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-fr-DXFxoYd-.js",
|
||||
"sha256": "4c1013cb61db49df76e856c34aea59266e065350210a5d964537f8dd42cf6774",
|
||||
"rawBytes": 2718,
|
||||
"gzipBytes": 1354,
|
||||
"path": "houseplan-assets/tools-fr-rOKrLY3t.js",
|
||||
"sha256": "69db41722abfa2f591aa865a8edbf1faaca6d1c727b3722c1896fd84e7c271d4",
|
||||
"rawBytes": 4136,
|
||||
"gzipBytes": 1357,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-ru-Di7X71yN.js",
|
||||
"sha256": "daa7bcbbde34f363c8b114b2af050cc19264e85c7bcc6ba31a13e0b963f4f8ce",
|
||||
"rawBytes": 3669,
|
||||
"gzipBytes": 1615,
|
||||
"path": "houseplan-assets/tools-ru-qWSdpJow.js",
|
||||
"sha256": "f78c576ce3f26c1b042e0c78131f305ef3a6d9c897d7fbf843f3386a44907ef0",
|
||||
"rawBytes": 5633,
|
||||
"gzipBytes": 1695,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-BbetEPvj.js",
|
||||
"sha256": "a204ece3a16a5af991ed3e6092e8ab2a3cd9d4d2bd79fdc1f056c504cdfa7798",
|
||||
"rawBytes": 10958,
|
||||
"gzipBytes": 4301,
|
||||
"path": "houseplan-assets/topology-de-wNwfkZjf.js",
|
||||
"sha256": "a35835cc2e0640b793f5d4b47f68bbe473ca6441da5891a8cc0ac88f2646db07",
|
||||
"rawBytes": 4237,
|
||||
"gzipBytes": 1888,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-fr-D9pmNHVr.js",
|
||||
"sha256": "353d6d4ac82acf236a4cbf85399586cc03de2ac488900061682e31853f3f7236",
|
||||
"rawBytes": 4321,
|
||||
"gzipBytes": 1869,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-ru-D6m0B05V.js",
|
||||
"sha256": "f73e4f25634c63aa2a05047910891e4f8eb83b18487cd833858ef6d5e171099f",
|
||||
"rawBytes": 5880,
|
||||
"gzipBytes": 2236,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-DtQtLxJ3.js",
|
||||
"sha256": "170d522fa5224dce0b9dbe9f2ed9178ea90c76250db39c52d5bdfe324bad8ae1",
|
||||
"rawBytes": 15905,
|
||||
"gzipBytes": 5952,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/topology-de-BjBB8qiK.js",
|
||||
"houseplan-assets/topology-fr-DXFxoYd-.js",
|
||||
"houseplan-assets/topology-ru-Di7X71yN.js"
|
||||
"houseplan-assets/topology-de-wNwfkZjf.js",
|
||||
"houseplan-assets/topology-fr-D9pmNHVr.js",
|
||||
"houseplan-assets/topology-ru-D6m0B05V.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-runtime-sjaKOGGU.js",
|
||||
"sha256": "bd75de4445ae8f036fbf89763dead2ce464783b566a6860fd9603e5c9251099b",
|
||||
"rawBytes": 3983,
|
||||
"gzipBytes": 1789,
|
||||
"path": "houseplan-assets/zigbee-topology-runtime-9_MBQQpH.js",
|
||||
"sha256": "7c0cb913d002611acac9ba45a3bf2ea7f6c3a07839769567975e8298c4210565",
|
||||
"rawBytes": 7883,
|
||||
"gzipBytes": 2877,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"houseplan-assets/zigbee-topology-BbetEPvj.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js",
|
||||
"houseplan-assets/namespace-language-COmALraj.js",
|
||||
"houseplan-assets/zigbee-topology-DtQtLxJ3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "efc308c02a842ebafa05186a59d8ad2d734445445f1b9b746e1914706dca87cf",
|
||||
"sha256": "cf76e68979acd8077ed8b389b5566ffd36e447a6d2598a09c80adc6d85d9cbb6",
|
||||
"rawBytes": 1205,
|
||||
"gzipBytes": 803,
|
||||
"gzipBytes": 804,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-panel.js",
|
||||
"sha256": "f820d606cf77d60252147bdf93075b2621d05e6cd3f014acdeb0c032b6b83d23",
|
||||
"sha256": "df9e9feb016b89df1e58abb42132832ca1f31676446fe3a70ba4c1fbe79eebf0",
|
||||
"rawBytes": 6165,
|
||||
"gzipBytes": 2324,
|
||||
"gzipBytes": 2326,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-DNuV5X02.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+64
-64
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
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";import{aD as e,l as o,aG as t,ey as s,A as a,ez as i,eA as l,eB as n,E as r,c}from"./houseplan-card-Ddo22-SB.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__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";import{aG as e,l as o,aJ as t,eE as s,A as a,eF as i,eG as l,eH as n,E as r,c}from"./houseplan-card-DNuV5X02.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}
|
||||
@@ -0,0 +1 @@
|
||||
function t(t){const a=t.visualViewport;return a&&Number.isFinite(a.width)&&Number.isFinite(a.height)?{left:Number(a.offsetLeft)||0,top:Number(a.offsetTop)||0,width:Math.max(0,Number(a.width)||0),height:Math.max(0,Number(a.height)||0)}:{left:0,top:0,width:t.innerWidth,height:t.innerHeight}}function a(t,a,h,e=7,i=8){const o=h.left+i,n=h.top+i,m=Math.max(o,h.left+h.width-i),r=Math.max(n,h.top+h.height-i),M=Math.max(0,m-o),d=Math.max(0,r-n),f=Math.min(Math.max(0,a.width),M),u=Math.min(Math.max(0,a.height),d);let p=t.left;p+f>m&&(p=t.right-f),p=Math.min(Math.max(o,p),Math.max(o,m-f));const x=t.bottom+e,s=t.top-e-u;let l="bottom",b=x;return x+u>r&&s>=n?(l="top",b=Math.min(Math.max(n,s),Math.max(n,r-u))):b=Math.min(Math.max(n,x),Math.max(n,r-u)),{left:Math.round(p),top:Math.round(b),side:l,maxWidth:Math.round(M),maxHeight:Math.round(d)}}function h(t){const a=t.HTMLElement?.prototype;return"function"==typeof a?.showPopover&&"function"==typeof a?.hidePopover}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{h as a,t as f,a as p};
|
||||
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
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+791
-765
File diff suppressed because one or more lines are too long
+9
-9
File diff suppressed because one or more lines are too long
-33
File diff suppressed because one or more lines are too long
+27
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
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={"led.tool":"LED-Streifen","led.hint":"Zeichnen Sie den Streifen Punkt für Punkt entlang der Wandflächen oder über den Boden; er rastet am Raster und an Wandflächen ein. Danach wählen Sie das Gerät, das er darstellt.","led.start":"Klicken, um den Streifen zu beginnen","led.start_touch":"Tippen, um den Streifen zu beginnen","led.points":"Punkte: {n} · Umschalt — 45°-Schritte · Esc — fertig · Strg+Z — Punkt entfernen","led.points_touch":"Punkte: {n} · Werkzeug wechseln zum Beenden","led.finish":"Fertig","led.stopped":"Der Streifen stößt an eine Wand","led.unbound":"Streifen ist mit keinem Gerät verbunden","led.bind":"Gerät verbinden","led.change":"Gerät wechseln","led.later":"Später","led.unbind":"Trennen","led.delete":"Streifen löschen","led.settings":"Geräteeinstellungen","led.type_note":"LED-Streifen: Form im Geräte-Editor auf dem Plan bearbeiten","led.show_strip":"Als LED-Streifen zeigen","led.show_icon":"Als Symbol zeigen","led.cancel":"Abbrechen","led.taken":"Bereits mit einem anderen LED-Streifen verbunden","led.space":"Zuerst den LED-Streifen lösen: Er gehört zu seinem Bereich","led.limit_points":"Höchstens 50 Punkte pro Streifen","led.limit_strips":"Höchstens 50 Streifen pro Bereich","led.pick_title":"Gerät für den Streifen wählen","led.pick_new":"Neues Gerät…","led.pick_empty":"Auf diesem Plan gibt es noch keine freien Geräte.","led.saved":"LED-Streifen gespeichert","led.save_failed":"Streifen nicht gespeichert: {err}","led.history_draw":"LED-Streifen","led.history_shape":"Form des LED-Streifens","led.history_view":"Darstellung des Geräts","led.optimize_walls":"Streifen durch Wände: {n} ({spaces})"};const n="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,n as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={"led.tool":"Ruban LED","led.hint":"Tracez le ruban point par point le long des faces des murs ou sur le sol ; il s’aimante à la grille et aux faces des murs. Ensuite, choisissez l’appareil qu’il représente.","led.start":"Cliquez pour commencer le ruban","led.start_touch":"Touchez pour commencer le ruban","led.points":"Points : {n} · Maj — pas de 45° · Échap — terminer · Ctrl+Z — retirer le point","led.points_touch":"Points : {n} · changez d’outil pour terminer","led.finish":"Terminer","led.stopped":"Le ruban bute contre un mur","led.unbound":"Ruban non lié à un appareil","led.bind":"Lier un appareil","led.change":"Changer d’appareil","led.later":"Plus tard","led.unbind":"Délier","led.delete":"Supprimer le ruban","led.settings":"Réglages de l’appareil","led.type_note":"Ruban LED : modifiez sa forme sur le plan dans l’éditeur d’appareils","led.show_strip":"Afficher en ruban LED","led.show_icon":"Afficher en icône","led.cancel":"Annuler","led.taken":"Déjà lié à un autre ruban LED","led.space":"Détachez d’abord le ruban LED : il appartient à son espace","led.limit_points":"50 points au maximum par ruban","led.limit_strips":"50 rubans au maximum par espace","led.pick_title":"Choisissez l’appareil du ruban","led.pick_new":"Nouvel appareil…","led.pick_empty":"Aucun appareil libre sur ce plan pour l’instant.","led.saved":"Ruban LED enregistré","led.save_failed":"Ruban non enregistré : {err}","led.history_draw":"Ruban LED","led.history_shape":"Forme du ruban LED","led.history_view":"Représentation de l’appareil","led.optimize_walls":"Rubans traversant des murs : {n} ({spaces})"};const a="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,a as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={"led.tool":"LED-лента","led.hint":"Нарисуйте ленту по точкам вдоль граней стен или по полу — она прилипает к сетке и граням. После завершения выберите устройство, которое она показывает.","led.start":"Нажмите, чтобы начать ленту","led.start_touch":"Коснитесь, чтобы начать ленту","led.points":"Точек: {n} · Shift — шаг 45° · Esc — завершить · Ctrl+Z — убрать точку","led.points_touch":"Точек: {n} · смените инструмент, чтобы завершить","led.finish":"Завершить","led.stopped":"Лента упёрлась в стену","led.unbound":"Лента не привязана к устройству","led.bind":"Привязать устройство","led.change":"Сменить устройство","led.later":"Позже","led.unbind":"Отвязать","led.delete":"Удалить ленту","led.settings":"Настройки устройства","led.type_note":"LED-лента: форма редактируется на плане в редакторе устройств","led.show_strip":"Показывать LED-лентой","led.show_icon":"Показывать значком","led.cancel":"Отмена","led.taken":"Уже привязано к другой LED-ленте","led.space":"Сначала отвяжите LED-ленту: она принадлежит своему пространству","led.limit_points":"Не более 50 точек в ленте","led.limit_strips":"Не более 50 лент в пространстве","led.pick_title":"Выберите устройство для ленты","led.pick_new":"Новое устройство…","led.pick_empty":"На этом плане пока нет свободных устройств.","led.saved":"LED-лента сохранена","led.save_failed":"Лента не сохранена: {err}","led.history_draw":"LED-лента","led.history_shape":"Форма LED-ленты","led.history_view":"Вид устройства","led.optimize_walls":"Лент, проходящих сквозь стены: {n} ({spaces})"};const d="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,d as fingerprint};
|
||||
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
@@ -0,0 +1,47 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";import{eu as e,eC as t,c6 as s,eJ as o,eK as i,ad as n,ee as a,O as r,eL as l,az as d,ef as c,aL as p,eM as u,eN as h,eO as f,A as m,eP as v}from"./houseplan-card-DNuV5X02.js";import{L as g,v as w,s as $,a as y,b as _,p as k,c as b,d as C,e as P}from"./led-strip-geometry-DCOEb7ps.js";const M="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";let F,L=null,U=null;function x(e,t){return L||U||F===e||""===F||(U=(void 0===F?import("./led-strip-field-aqwdQqPw.js"):import(new URL(`./led-strip-field-aqwdQqPw.js?${Date.now()}`,import.meta.url).href)).then(e=>{e.LED_FIELD_FINGERPRINT===o?L=e:F=""},()=>{F=e}).finally(()=>{U=null})),L||U?.then(()=>L&&t()),L}const D=e=>e.some(e=>e.glow&&"on"===e.state),R=e=>e.map(e=>[e[0],e[1]]),E="#383838";function A(o,i){if(!o)return null;const n=Array.isArray(o.masonryGeometry)?o.masonryGeometry:[],a=[...e(o.masonryGeometry),...o.opaqueBodies],r=[];for(const e of a)for(let t=0;t<e.length;t++){const s=e[t],o=e[(t+1)%e.length];s&&o&&r.push({a:[s[0],s[1]],b:[o[0],o[1]]})}const l=25,d=new Map;r.forEach((e,t)=>{const s=Math.floor(Math.min(e.a[0],e.b[0])/l),o=Math.floor(Math.max(e.a[0],e.b[0])/l),i=Math.floor(Math.min(e.a[1],e.b[1])/l),n=Math.floor(Math.max(e.a[1],e.b[1])/l);for(let e=s;e<=o;e++)for(let s=i;s<=n;s++){const o=`${e},${s}`,i=d.get(o);i?i.push(t):d.set(o,[t])}});const c=e=>{let t=1/0,s=1/0,o=-1/0,i=-1/0;for(const n of e)t=Math.min(t,n[0]),s=Math.min(s,n[1]),o=Math.max(o,n[0]),i=Math.max(i,n[1]);return[t,s,o,i]},p=(e,t)=>e[0]>=t[0]&&e[0]<=t[2]&&e[1]>=t[1]&&e[1]<=t[3],u=n.filter(e=>e?.[0]?.length).map(e=>({box:c(e[0]),poly:e})),h=o.opaqueBodies.map(e=>({box:c(e),body:e}));return{faces:r,inside:e=>u.some(({box:s,poly:o})=>p(e,s)&&t([e[0],e[1]],[o]))||h.some(({box:t,body:o})=>p(e,t)&&s([e[0],e[1]],o)),epsilon:i,near:e=>{const t=new Set;for(let s=Math.floor(e[0]/l);s<=Math.floor(e[2]/l);s++)for(let o=Math.floor(e[1]/l);o<=Math.floor(e[3]/l);o++)for(const e of d.get(`${s},${o}`)||[])t.add(e);return[...t].map(e=>r[e])}}}function I(e){const t=e.candidate?.sourceEid||e.device.primary||"",s=t?e.hass?.states?.[t]?.state:void 0,o="unavailable"===s||"unknown"===s||!!t&&!!e.hass&&!e.hass.states?.[t]&&!e.device.virtual,i=Number(e.device.marker?.glow_radius_cm),n=Number.isFinite(i)&&i>0?i/e.cellCm*e.gridPitch:e.defaultRadius,a=o?null:e.candidate?.appearance??null;return{strip:e.strip,device:e.device,state:o?"unavailable":a?"on":"off",glow:e.glow,appearance:a,radius:n}}const T=new WeakMap;function j(e,t,s){const o=P*s;const i=T.get(e);if(i&&i.faces===t&&i.epsilon===t?.epsilon&&i.value.t===o&&i.points.length===e.points.length&&i.points.every((t,s)=>t[0]===e.points[s][0]&&t[1]===e.points[s][1]))return i.value;const n=R(e.points),a=_(n,t,o/2),r={t:o,path:a,d:k(a)};return T.set(e,{faces:t,epsilon:t?.epsilon,points:n,value:r}),r}function G(e){if(!e.views.length)return d``;const t=e.handlers,s=[...e.views].sort((e,t)=>e.strip.id<t.strip.id?1:e.strip.id>t.strip.id?-1:0),o=e.iso;return d`<g class="led-strips" data-hp-led-strips="${s.length}"
|
||||
data-hp-iso=${o?"raised":m}>
|
||||
${o?d`<defs><filter id="hp-led-iso-shadow" filterUnits="userSpaceOnUse"
|
||||
x="-100000" y="-100000" width="200000" height="200000">
|
||||
<feGaussianBlur stdDeviation="${o.shadow.sigma}"></feGaussianBlur></filter></defs>`:m}
|
||||
<style>
|
||||
.led-strip .led-focus { fill: none; stroke: transparent; }
|
||||
.led-strip:has(.led-hit:focus-visible) .led-focus { stroke: var(--primary-color, #03a9f4); }
|
||||
.led-strip .led-hit { fill: none; stroke: transparent; cursor: pointer; outline: none; }
|
||||
</style>
|
||||
${v(s,e=>e.strip.id,s=>{const{t:i,path:n,d:a}=j(s.strip,e.faces,e.d),r="unavailable"===s.state,l=r?"#9e9e9e":"on"===s.state&&s.appearance?s.appearance.c:"#FFFFFF",c=2*b(i*e.perUnit)/(e.perUnit||1),p=s.device,u=t=>function(e,t,s){const o=e.currentTarget,i=o?.getScreenCTM?.();if(!i||s.views.length<2)return t;const n=i.inverse(),a=[n.a*e.clientX+n.c*e.clientY+n.e,n.b*e.clientX+n.d*e.clientY+n.f],r=s.views.map(e=>{const{t:t,path:o}=j(e.strip,s.faces,s.d);return{id:e.strip.id,points:o.points.map(e=>[e[0]*s.perUnit,e[1]*s.perUnit]),closed:o.closed,thicknessPx:t*s.perUnit}}),l=C([a[0]*s.perUnit,a[1]*s.perUnit],r);return s.views.find(e=>e.strip.id===l)||t}(t,s,e).device,h=o?`translate(0 ${-o.lift})`:m;return d`<g class="led-strip state-${s.state}" data-led-strip="${s.strip.id}"
|
||||
data-marker="${p.id}" data-state="${s.state}" data-closed="${n.closed?"true":"false"}">
|
||||
${o?d`<path class="led-iso-shadow" d="${a}" fill="none" stroke="black"
|
||||
stroke-opacity="${o.shadow.opacity}" stroke-width="${i}" stroke-linecap="round"
|
||||
stroke-linejoin="round" filter="url(#hp-led-iso-shadow)" pointer-events="none"
|
||||
transform="translate(${o.shadow.dx} ${o.shadow.dy-o.lift})"></path>
|
||||
${[1,.5].map(e=>d`<path class="led-iso-edge" d="${a}" fill="none" stroke="${o.edge}"
|
||||
stroke-width="${i}" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"
|
||||
transform="translate(0 ${-o.lift+o.depth*e})"></path>`)}`:m}
|
||||
<g transform=${h}>
|
||||
<path class="led-outline" d="${a}" fill="none" stroke="${E}" stroke-width="${i}"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
|
||||
<path class="led-core" d="${a}" fill="none" stroke="${l}" stroke-width="${i/2}"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
|
||||
<path class="led-focus" d="${a}" stroke-width="${1.6*i}" stroke-linecap="round"
|
||||
stroke-linejoin="round" aria-hidden="true"></path>
|
||||
${t?d`<path class="led-hit" d="${a}" stroke-width="${c}"
|
||||
stroke-linecap="round" stroke-linejoin="round" pointer-events="stroke"
|
||||
role="button" tabindex="0" aria-label="${t.label(p)}"
|
||||
data-hp="device" data-id="${p.id}"
|
||||
@click=${e=>t.click(e,u(e))}
|
||||
@keydown=${e=>t.keydown(e,p)}
|
||||
@contextmenu=${e=>t.contextmenu(e,u(e))}
|
||||
@pointerdown=${e=>t.pointerdown(e,u(e))}
|
||||
@pointermove=${e=>t.pointermove(e,u(e))}
|
||||
@pointerup=${e=>t.pointerup(e,u(e))}
|
||||
@pointercancel=${e=>t.pointercancel(e,p)}
|
||||
@lostpointercapture=${e=>t.pointercancel(e,p)}
|
||||
@pointerover=${e=>t.pointerover(e,u(e))}
|
||||
@pointerleave=${()=>t.pointerleave()}
|
||||
@focus=${e=>t.focus(e,p)}
|
||||
@blur=${()=>t.blur(p)}></path>`:m}
|
||||
</g>
|
||||
</g>`})}
|
||||
</g>`}function H(e,t,s,o){return(t?e.find(e=>e.id===t):void 0)??(s?e.find(e=>o(s,e)):void 0)}function N(e){const t=new Map(e.devices.map(e=>[e.id,e])),s=g/e.cellCm*e.gridPitch,o=new Map(i({hass:e.hass,devices:e.devices,virtualLights:e.virtualLights,spaceId:e.space.id,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,defaultRadiusUnits:s,cellCm:e.cellCm,gridPitch:e.gridPitch,position:()=>({x:0,y:0})}).map(e=>[e.key,e])),r=[];for(const i of e.space.led_strips||[]){if(!1===i?.active||!i.marker||!w(i.points))continue;const a={...i,points:i.points.map(e=>[e[0]*n,e[1]*n])},l=t.get(a.marker);if(!l||l.space!==e.space.id)continue;if(l.hidden&&!e.showHidden)continue;const d=H(e.space.rooms,l.marker?.room_id,$(R(a.points)),e.inRoom);r.push(I({strip:a,device:l,candidate:o.get(`${e.space.id}|${l.id}`)??null,hass:e.hass,glow:!!d&&e.glowFor(d),defaultRadius:s,cellCm:e.cellCm,gridPitch:e.gridPitch}))}const l=y/e.cellCm*e.gridPitch;return{views:r,faces:A(e.scene,l),d:e.iconPct/100*a(e.space),scene:e.scene,polygons:e.polygons}}const q=new WeakMap;function B(e){for(const t of q.get(e)?.frame.views??[])T.delete(t.strip);q.delete(e),L?.releaseLedField(e)}function S(e){return{shapes:q.get(e)?.frame.views.length??0,...L?.ledFieldStats(e)??{visibility:0,sources:0,visibilityPaths:0,pathChars:0,recomputes:0}}}function O(e,t,s){const o=t.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),i=o.length?e._lightBarriers(t,o.map(({room:e,poly:t})=>({r:e,poly:t}))):null,n=[t,e._renderDevices,e._renderPlanHass,s,i,e._mode,e._showAll],a=q.get(e);if(a&&a.key.every((e,t)=>e===n[t]))return a.frame;for(const e of a?.frame.views??[])T.delete(e.strip);const d=e._config?.icon_size??2.5,c=N({space:t,devices:e._renderDevices,hass:e._renderPlanHass,virtualLights:e._virtualLights,defaultColor:e._fillColors.glow_light.c,paletteAlpha:e._fillColors.glow_light.a,cellCm:e._cellCm,gridPitch:e._gridPitch,iconPct:d>8?2.5:d,scene:i,polygons:o,glowFor:e=>l(s,e),inRoom:(t,s)=>e._pointInRoom(t,s),showHidden:"devices"===e._mode&&e._showAll});return!1!==e.isConnected&&q.set(e,{key:n,frame:c}),c}function W(e,t,s,o){const i=O(e,t,s),n=e._stageEl?.clientWidth,a="iso"===e._renderProjection&&"view"===e._mode,r=a?i.d*c:i.d,l="theme-dark"===p(e._renderPlanHass)?"dark":"light",m=!!e._isoLightFloors?.size,v=u(l,m),g=G({views:i.views,d:r,iso:a?{lift:f.lift*r,depth:f.depth*r,edge:h(E,l,m),shadow:{dx:v.dx*r,dy:v.dy*r,sigma:v.sigma*r,opacity:v.opacityWhite}}:null,faces:i.faces,perUnit:n&&o.w?n/o.w:1,handlers:"view"===e._mode?{click:(t,s)=>e._clickDevice(t,s),keydown:(t,s)=>e._keyDevice(t,s),contextmenu:(t,s)=>e._ctxDevice(t,s),pointerdown:(t,s)=>e._pointerDown(t,s),pointermove:(t,s)=>{const o=e._pointerMove(t,s);o&&e._showDeviceTip(t,o)},pointerup:(t,s)=>e._pointerUp(t,s),pointercancel:(t,s)=>e._pointerCancel(t,s),pointerover:(t,s)=>e._showDeviceTip(t,s),pointerleave:()=>e._clearPointerHover(),focus:(t,s)=>e._showDeviceFocusTip(t,s),blur:t=>e._hideDeviceFocusTip(t.id),label:e=>e.name}:null});return"plan"===e._mode||"decor"===e._mode?d`<g class="led-passive" opacity="0.45" pointer-events="none">${g}</g>`:g}function z(e,t,s){if(!1===e.isConnected)return d``;const o=O(e,t,s),i=o.scene&&(D(o.views)||L?.hasLedField(e))?L||x(t.id,()=>!1!==e.isConnected&&e.requestUpdate()):null;return i?i.renderLedField({views:o.views,scene:o.scene,polygons:o.polygons,faces:o.faces,spaceId:t.id,owner:e,requestUpdate:()=>e.requestUpdate(),isConnected:()=>!1!==e.isConnected}):d``}function Y(e){const t=e.space.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),s=N({space:e.space,devices:e.devices,hass:e.hass,virtualLights:e.virtualLights,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,cellCm:e.cellCm,gridPitch:e.gridPitch,iconPct:e.iconPct,scene:e.scene,polygons:t,glowFor:e.glowFor,inRoom:e.inRoom,showHidden:!1}),o=e.live?s.views:s.views.map(e=>({...e,state:"off",appearance:null,glow:!1})),i=A({masonryGeometry:e.bodies.masonryGeometry,opaqueBodies:e.bodies.opaqueBodies},y/e.cellCm*e.gridPitch),n=e.isConnected()&&e.scene&&e.live&&(D(o)||L?.hasLedField(e.owner))?x(e.space.id,()=>e.isConnected()&&e.ready()):null;return n||L?.releaseLedField(e.owner),d`${n?n.renderLedField({views:o,scene:e.scene,polygons:t,faces:i,spaceId:e.space.id,owner:e.owner,requestUpdate:e.ready,isConnected:e.isConnected}):m}${G({views:o,d:s.d,faces:i,perUnit:e.perUnit,handlers:null})}`}export{M as LED_RUNTIME_FINGERPRINT,A as faceContext,x as ledField,N as ledFrame,O as ledFrameFor,S as ledStats,I as ledStripView,j as ledStripePath,B as releaseLed,z as renderLedFieldFor,W as renderLedLayerFor,G as renderLedStripes,Y as renderStaticLed,H as stripRoom};
|
||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";const t=t=>[t.left,t.top,t.right,t.bottom,t.width,t.height].every(Number.isFinite)&&t.right>=t.left&&t.bottom>=t.top&&t.width>=0&&t.height>=0;function e(e){const{preferred:o,size:i,bounds:h,blockers:r,gap:n=8,edge:d=8}=e;if(![o.x,o.y,i.width,i.height,n,d].every(Number.isFinite)||i.width<=0||i.height<=0||n<0||d<0||!t(h)||r.some(e=>!t(e)))return null;const f=h.left+d,s=h.right-d-i.width,l=h.top+d,a=h.bottom-d-i.height;if(![f,s,l,a].every(Number.isFinite)||s<f||a<l)return null;const g=r.filter(t=>t.right>t.left&&t.bottom>t.top).map(t=>({left:t.left-n-i.width,right:t.right+n,top:t.top-n-i.height,bottom:t.bottom+n})),b=(t,e,o)=>Math.max(e,Math.min(o,t)),m=new Set([f,s,b(o.x,f,s)]),p=new Set([l,a,b(o.y,l,a)]);for(const t of g)m.add(b(t.left,f,s)),m.add(b(t.right,f,s)),p.add(b(t.top,l,a)),p.add(b(t.bottom,l,a));const c=[...m].sort((t,e)=>t-e),u=[...p].sort((t,e)=>t-e);let w=null,y=1/0;for(const t of u)for(const e of c){if(g.some(o=>e>o.left&&e<o.right&&t>o.top&&t<o.bottom))continue;const i=Math.hypot(e-o.x,t-o.y);(!w||i<y)&&(w={left:e,top:t},y=i)}return w}export{e as p};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";import{eR as e,eS as a,eG as d}from"./houseplan-card-DNuV5X02.js";function n(d,n){return new e([{code:"en",dictionary:d},{code:"ru",loadDictionary:n.ru},{code:"de",loadDictionary:n.de},{code:"fr",loadDictionary:n.fr}],"06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698",console.warn,a)}function o(e){return function(e,a){return{state:d=>{const n=e.state(d);return"pending"!==n&&a.some(e=>"pending"===e.state(d))?"pending":n},dictionary:a=>e.dictionary(a),ensure:d=>Promise.all([e,...a].map(e=>e.ensure(d))).then(()=>{})}}(d,e)}export{n,o as s};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";import{eD as e,eE as n,eA as c}from"./houseplan-card-Ddo22-SB.js";function a(c,a){return new e([{code:"en",dictionary:c},{code:"ru",loadDictionary:a.ru},{code:"de",loadDictionary:a.de},{code:"fr",loadDictionary:a.fr}],"d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96",console.warn,n)}function d(e){return function(e,n){return{state:c=>{const a=e.state(c);return"pending"!==a&&n.some(e=>"pending"===e.state(c))?"pending":a},dictionary:n=>e.dictionary(n),ensure:c=>Promise.all([e,...n].map(e=>e.ensure(c))).then(()=>{})}}(c,e)}export{a as n,d as s};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";import{aG as e,l as t,aJ as s,A as a,eF as o,eG as i,eH as n,E as l,eQ as c,c as r}from"./houseplan-card-DNuV5X02.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!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}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return t(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"fit",selector:{select:{mode:"dropdown",options:[{value:"content",label:s(this._lang,"editor.fit_content")},{value:"house",label:s(this._lang,"editor.fit_house")}]}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"light_pools",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}}]}render(){if(!this.hass||!this._config)return a;const e=o(this,i,t(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return l;this._loadSpaces();const h=this._lang,_={space:s(h,"editor.space"),title:s(h,"editor.title"),fit:s(h,"editor.framing"),show_button:s(h,"editor.show_button"),button_label:s(h,"editor.button_label"),button_target:s(h,"editor.button_target"),icon_size:s(h,"editor.icon_size"),show_temperature:s(h,"editor.show_temperature"),live_states:s(h,"editor.live_states"),light_pools:s(h,"editor.light_pools"),show_signal:s(h,"editor.show_signal")};return r`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${{...this._config,fit:c(this._config.fit)}}
|
||||
.schema=${this._schema}
|
||||
.computeLabel=${e=>_[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const s=new Event("config-changed",{bubbles:!0,composed:!0});s.detail={config:t},this.dispatchEvent(s)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",h);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";import{c as a,A as e}from"./houseplan-card-Ddo22-SB.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-jjP-5y3_.js";import"./form-kit.styles-CCrtJjwP.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__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";import{c as a,A as e}from"./houseplan-card-DNuV5X02.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-l2frpvJL.js";import"./form-kit.styles-DNNRm1qV.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
<span class="summary-switch-caption"><strong>${b("summary.show_local")}</strong>
|
||||
<small id="summary-local-show-hint">${b("summary.show_local_hint")}</small></span>
|
||||
<input id="summary-local-show" data-summary-local-show type="checkbox"
|
||||
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";import{eG as e,N as t,b2 as n,L as i,ae as o,ac as r,ca as s,eH as a,c9 as c,b1 as l,b0 as d,b3 as u,eI as f,eJ as m,cc as g,eK as v}from"./houseplan-card-Ddo22-SB.js";function y(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),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 _(e,t){return e?.length?t?.length?v(e,t):e:t}function h(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:d(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*p(l,d,u=h){try{let f=0;for(const m of d){const d=l.spaces.find(e=>String(e?.id)===m.id);if(!d)continue;const g=e(d,m),v=u(m,g);let y=null;for(const e of m.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(m.rooms,e.id,g.walls,g.openCuts,i,g.cellCm,o,r,v.roomGeom,v.multiWallNodes)||a;y=_(y,s(c,g.physicalBodies)),yield}const h=a(y||[],m.stairs);let p=h.next();for(;!p.done;)yield,p=h.next();y=p.value;const b=g.cellCm/o;f+=c(y)*b*b/1e4}return f}catch{return null}}function b(e,t,n=h){const i=p(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:g(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 C(e,t){const n=e?.states?.[t];if(!n)return null;const i=f(e,t);return i?m(i,String(n.attributes?.unit_of_measurement||"")):null}export{p as cleanFloorAreaSteps,y as representedHaDeviceIds,h as spaceWallGeometry,C as summaryEntityValue,S as summarySystemValue,b as totalCleanFloorAreaM2};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";import{e$ as e,O as t,b5 as n,M as i,af as o,ad as r,cf as s,f0 as a,ce as l,b4 as c,b3 as d,b6 as u,f1 as f,f2 as m,ch as g,f3 as v}from"./houseplan-card-DNuV5X02.js";function y(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),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,l="entity"===r?e.registry.entities?.[s]:null,c="device"===r?s:l?.device_id,d="device"===r?a?.area_id:l?.area_id||c&&e.registry.devices?.[c]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;c&&e.registry.devices?.[c]&&e.spaceIds.has(f)&&(!n.devices.has(c)||"entity"===r&&n.liveEntities.has(s))&&t.add(c)}return t}function _(e,t){return e?.length?t?.length?v(e,t):e:t}function h(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:d(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*p(c,d,u=h){try{let f=0;for(const m of d){const d=c.spaces.find(e=>String(e?.id)===m.id);if(!d)continue;const g=e(d,m),v=u(m,g);let y=null;for(const e of m.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const l=n(m.rooms,e.id,g.walls,g.openCuts,i,g.cellCm,o,r,v.roomGeom,v.multiWallNodes)||a;y=_(y,s(l,g.physicalBodies)),yield}const h=a(y||[],m.stairs);let p=h.next();for(;!p.done;)yield,p=h.next();y=p.value;const b=g.cellCm/o;f+=l(y)*b*b/1e4}return f}catch{return null}}function b(e,t,n=h){const i=p(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:g(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 C(e,t){const n=e?.states?.[t];if(!n)return null;const i=f(e,t);return i?m(i,String(n.attributes?.unit_of_measurement||"")):null}export{p as cleanFloorAreaSteps,y as representedHaDeviceIds,h as spaceWallGeometry,C as summaryEntityValue,S as summarySystemValue,b as totalCleanFloorAreaM2};
|
||||
+6
-6
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={"furn.title":"Möbelbibliothek","furn.symbol":"Symbol","furn.group_furniture":"Möbel","furn.group_appliance":"Geräte","furn.group_sanitary":"Sanitär","furn.group_other":"Sonstiges","furn.width":"Breite","furn.depth":"Tiefe","furn.back_to_categories":"Alle Kategorien","furn.pick_hint":"Wählen Sie eine Kategorie und dann ein Symbol.","furn.place_hint":"Klicken Sie auf den Plan — das Element wird an der nächsten Wand platziert. Mit Shift wird es frei platziert.","furn.cat_air_conditioner":"Klimaanlagen","furn.cat_armchair":"Sessel","furn.cat_bathtub":"Badewannen","furn.cat_bed":"Betten","furn.cat_bidet":"Bidets","furn.cat_boiler":"Warmwasserbereiter","furn.cat_chair":"Stühle","furn.cat_coffee_table":"Couchtische","furn.cat_computer":"Computer","furn.cat_cooktop":"Kochfelder","furn.cat_dining_table":"Esstische","furn.cat_dishwasher":"Geschirrspüler","furn.cat_dryer":"Wäschetrockner","furn.cat_exercise":"Fitnessgerät","furn.cat_fireplace":"Kamine","furn.cat_fridge":"Kühlschränke","furn.cat_hood":"Dunstabzugshaube","furn.cat_kitchen_cabinet":"Küchenschränke","furn.cat_kitchen_sink":"Küchenspülen","furn.cat_nightstand":"Schränke","furn.cat_oven":"Backofen","furn.cat_plant":"Pflanzen","furn.cat_rug":"Teppiche","furn.cat_shelving":"Regale","furn.cat_shower":"Duschen","furn.cat_sink":"Waschbecken","furn.cat_sofa":"Sofas","furn.cat_stairs":"Treppen","furn.cat_toilet":"Toiletten","furn.cat_tv":"Fernseher","furn.cat_wardrobe":"Kleiderschränke","furn.cat_washer":"Waschmaschinen","furn.cat_work_table":"Schreibtische","furn.sym_exercise":"Fitnessgerät","furn.sym_computer":"Computer","furn.sym_hood":"Dunstabzugshaube","furn.sym_oven":"Backofen","furn.sym_sofa":"Zweisitzer-Sofa","furn.sym_sofa_three_seat":"Dreisitzer-Sofa","furn.sym_sofa_corner_right":"Ecksofa rechts","furn.sym_armchair":"Polstersessel","furn.sym_armchair_office":"Bürostuhl","furn.sym_coffee_table":"Rechteckiger Couchtisch","furn.sym_coffee_table_round":"Runder Couchtisch","furn.sym_coffee_table_oval":"Ovaler Couchtisch","furn.sym_coffee_table_rounded":"Abgerundeter Couchtisch","furn.sym_table_dining":"Rechteckiger Esstisch","furn.sym_table_round":"Rundtisch","furn.sym_table_dining_oval":"Ovaler Esstisch","furn.sym_table_dining_rounded":"Abgerundeter Esstisch","furn.sym_chair":"Stuhl","furn.sym_chair_bar":"Barhocker","furn.sym_desk":"Rechteckiger Schreibtisch","furn.sym_desk_corner":"Eckschreibtisch","furn.sym_bed_double":"Doppelbett","furn.sym_bed_single":"Einzelbett","furn.sym_nightstand":"Nachttisch","furn.sym_cabinet_tv":"TV-Schrank","furn.sym_cabinet_shoe":"Schuhschrank","furn.sym_cabinet_sink":"Waschtischunterschrank","furn.sym_wardrobe":"Kleiderschrank","furn.sym_bookshelf":"Bücherschrank","furn.sym_wall_unit":"Schrankwand","furn.sym_kitchen_floor":"Küchen-Unterschrank","furn.sym_kitchen_floor_corner":"Küchen-Eckunterschrank","furn.sym_kitchen_wall":"Küchen-Hängeschrank","furn.sym_kitchen_wall_corner":"Küchen-Eckhängeschrank","furn.sym_shelf_floor":"Standregal","furn.sym_shelf_wall":"Wandregal","furn.sym_fridge":"Kühlschrank","furn.sym_stove":"Vierflammiges Kochfeld","furn.sym_cooktop_two":"Zweiflammiges Kochfeld","furn.sym_dishwasher":"Geschirrspüler","furn.sym_washer":"Waschmaschine","furn.sym_dryer":"Wäschetrockner","furn.sym_tv":"Fernseher mit Standfuß","furn.sym_tv_wall":"Wandfernseher","furn.sym_ac":"Klimaanlage","furn.sym_water_heater":"Wassererhitzer","furn.sym_toilet":"Stand-WC","furn.sym_toilet_built_in":"Wand-WC","furn.sym_bathtub":"Rechteckige Badewanne","furn.sym_bathtub_corner":"Eckbadewanne","furn.sym_shower":"Dusche","furn.sym_sink":"Waschbecken","furn.sym_kitchen_sink":"Einzel-Küchenspüle","furn.sym_kitchen_sink_double":"Doppel-Küchenspüle","furn.sym_bidet":"Stand-Bidet","furn.sym_bidet_built_in":"Wand-Bidet","furn.sym_stairs":"Treppe","furn.sym_fireplace":"Kamin","furn.sym_plant":"Pflanze","furn.sym_rug":"Teppich"};const r="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,r as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={"furn.title":"Bibliothèque de mobilier","furn.symbol":"Symbole","furn.group_furniture":"Mobilier","furn.group_appliance":"Électroménager","furn.group_sanitary":"Sanitaires","furn.group_other":"Autre","furn.width":"Largeur","furn.depth":"Profondeur","furn.back_to_categories":"Toutes les catégories","furn.pick_hint":"Choisissez un symbole, puis cliquez sur le plan.","furn.place_hint":"Cliquez sur le plan — le meuble se place contre le mur le plus proche. Maj permet un placement libre.","furn.cat_air_conditioner":"Climatiseurs","furn.cat_armchair":"Fauteuils","furn.cat_bathtub":"Baignoires","furn.cat_bed":"Lits","furn.cat_bidet":"Bidets","furn.cat_boiler":"Chauffe-eau","furn.cat_chair":"Chaises","furn.cat_coffee_table":"Tables basses","furn.cat_computer":"Ordinateur","furn.cat_cooktop":"Plaques de cuisson","furn.cat_dining_table":"Tables à manger","furn.cat_dishwasher":"Lave-vaisselle","furn.cat_dryer":"Sèche-linge","furn.cat_exercise":"Équipement de sport","furn.cat_fireplace":"Cheminées","furn.cat_fridge":"Réfrigérateurs","furn.cat_hood":"Hotte","furn.cat_kitchen_cabinet":"Meubles de cuisine","furn.cat_kitchen_sink":"Éviers de cuisine","furn.cat_nightstand":"Meubles","furn.cat_oven":"Four","furn.cat_plant":"Plantes","furn.cat_rug":"Tapis","furn.cat_shelving":"Étagères","furn.cat_shower":"Douches","furn.cat_sink":"Lavabos","furn.cat_sofa":"Canapés","furn.cat_stairs":"Escaliers","furn.cat_toilet":"Toilettes","furn.cat_tv":"Téléviseurs","furn.cat_wardrobe":"Armoires","furn.cat_washer":"Lave-linge","furn.cat_work_table":"Bureaux","furn.sym_exercise":"Équipement de sport","furn.sym_computer":"Ordinateur","furn.sym_hood":"Hotte","furn.sym_oven":"Four","furn.sym_sofa":"Canapé","furn.sym_sofa_three_seat":"Canapé trois places","furn.sym_sofa_corner_right":"Canapé d'angle droit","furn.sym_armchair":"Fauteuil","furn.sym_armchair_office":"Chaise de bureau","furn.sym_coffee_table":"Table basse","furn.sym_coffee_table_round":"Table basse ronde","furn.sym_coffee_table_oval":"Table basse ovale","furn.sym_coffee_table_rounded":"Table basse arrondie","furn.sym_table_dining":"Table à manger","furn.sym_table_round":"Table ronde","furn.sym_table_dining_oval":"Table à manger ovale","furn.sym_table_dining_rounded":"Table à manger arrondie","furn.sym_chair":"Chaise","furn.sym_chair_bar":"Tabouret de bar","furn.sym_desk":"Bureau","furn.sym_desk_corner":"Bureau d'angle","furn.sym_bed_double":"Lit double","furn.sym_bed_single":"Lit simple","furn.sym_nightstand":"Table de chevet","furn.sym_cabinet_tv":"Meuble TV","furn.sym_cabinet_shoe":"Meuble à chaussures","furn.sym_cabinet_sink":"Meuble sous lavabo","furn.sym_wardrobe":"Armoire","furn.sym_bookshelf":"Bibliothèque","furn.sym_wall_unit":"Élément mural","furn.sym_kitchen_floor":"Module de cuisine bas","furn.sym_kitchen_floor_corner":"Module de cuisine bas d'angle","furn.sym_kitchen_wall":"Module de cuisine haut","furn.sym_kitchen_wall_corner":"Module de cuisine haut d'angle","furn.sym_shelf_floor":"Étagère au sol","furn.sym_shelf_wall":"Étagère murale","furn.sym_fridge":"Réfrigérateur","furn.sym_stove":"Cuisinière","furn.sym_cooktop_two":"Plaque à deux feux","furn.sym_dishwasher":"Lave-vaisselle","furn.sym_washer":"Lave-linge","furn.sym_dryer":"Sèche-linge","furn.sym_tv":"Téléviseur","furn.sym_tv_wall":"Téléviseur mural","furn.sym_ac":"Climatiseur","furn.sym_water_heater":"Chauffe-eau","furn.sym_toilet":"Toilettes","furn.sym_toilet_built_in":"Toilettes suspendues","furn.sym_bathtub":"Baignoire","furn.sym_bathtub_corner":"Baignoire d'angle","furn.sym_shower":"Douche","furn.sym_sink":"Lavabo","furn.sym_kitchen_sink":"Évier de cuisine","furn.sym_kitchen_sink_double":"Évier de cuisine double","furn.sym_bidet":"Bidet","furn.sym_bidet_built_in":"Bidet suspendu","furn.sym_stairs":"Escalier","furn.sym_fireplace":"Cheminée","furn.sym_plant":"Plante","furn.sym_rug":"Tapis"};const r="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,r as fingerprint};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";var e={title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",help:"Zeigt, mit welchen Zigbee-Geräten jedes Gerät direkt verbunden ist. Die Linienfarbe ist die Verbindungsqualität (LQI) auf derselben Skala wie der LQI-Wert des Geräts: rot bei 40 und darunter, grün bei 180 und darüber. Eine gestrichelte Linie bedeutet, dass die Qualität nicht gemeldet wurde. Ein Pfeil zeigt auf das nächste Gerät auf dem Weg zum Koordinator: ein Pfeil führt hinaus, eingehende Pfeile sind die Geräte, die über dieses Gerät routen. Eine Linie ohne Pfeil ist ein Ersatznachbar. Das ist der Routenbaum, den House Plan bildet, nicht der Weg eines Pakets in diesem Moment: eine Beschriftung am Pfeilende bedeutet, dass das Ziel nicht auf diesem Plan liegt, und ein fehlender Pfeil, dass die Route unbekannt ist. Die Daten stammen aus dem letzten Laden und veralten; Verbindungen sind nur mit der Maus sichtbar.",help_aria:"Hilfe: Zigbee-Verbindungen",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan kann 10 Sekunden bis 2 Minuten dauern und Zigbee vorübergehend verlangsamen.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · einige Knoten fehlen",status_no_links:"Empfangen: {time} · keine beobachteten Verbindungen gefunden",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich"};const n="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";export{e as dictionary,n as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Zeigt Eltern und nächste Hops laut Integration. Beim Darüberfahren werden keine Daten abgerufen.",help:"Zeigt den Elternknoten eines Endgeräts oder den aktiven nächsten Hop eines Routers zum Koordinator laut Integration. Es gibt höchstens einen ausgehenden Pfeil; eingehende Pfeile zeigen Geräte, die über diesen Knoten routen. Gewöhnliche Nachbarn werden ausgeblendet. Alle Linien sind durchgezogen. Linie und Pfeil verwenden LQI 0–255: rot bei 0, gelb bei 128, grün bei 255. Ohne LQI sind sie grau mit schwarzer Kontur. Eine Beschriftung am Pfeilende nennt ein Ziel in einem anderen Bereich oder außerhalb des Plans. Keine Routendaten bedeutet fehlende eindeutige Angaben, nicht dass das Gerät offline ist. Dies sind zuletzt empfangene Integrationsdaten, keine Live-Paketverfolgung oder Garantie des gesamten Pfads. Der ZHA-Cache kann beim Lesen bereits alt sein; veraltete und unvollständige Daten werden markiert. Verbindungen sind nur mit der Maus sichtbar.",help_aria:"Hilfe: Zigbee-Verbindungen",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan mit Routen kann länger als 10 Minuten dauern. House Plan wartet im Hintergrund weiter. Der Scan kann Zigbee vorübergehend verlangsamen.",scan_connecting:"Verbindung zu Zigbee2MQTT wird hergestellt",scan_waiting:"Warten auf die Karte von Zigbee2MQTT",scan_elapsed:"Vergangen: {time}",scan_background_hint:"Du kannst die Einstellungen schließen — das Warten wird im Hintergrund fortgesetzt.",scan_long_wait:"Zigbee2MQTT hat noch nicht geantwortet. Das Warten wird fortgesetzt.",scan_cancel:"Anfrage abbrechen",scan_cancel_hint:"Nur das Warten von House Plan wird beendet. Zigbee2MQTT kann den Scan fortsetzen.",scan_cancelled:"Warten abgebrochen. Der Scan von Zigbee2MQTT ist möglicherweise noch nicht beendet.",scan_backend_required:"Aktualisieren Sie die House-Plan-Integration und starten Sie Home Assistant neu, um Kartenscans im Hintergrund zu nutzen.",scan_connection_lost:"Die MQTT-Verbindung wurde unterbrochen; das Ergebnis ist möglicherweise nicht angekommen. Das Warten wurde beendet, Zigbee2MQTT scannt möglicherweise weiter.",scan_frontend_disconnected:"Die Verbindung zu Home Assistant wurde unterbrochen. Das Warten wird im Hintergrund fortgesetzt. Stelle die Verbindung wieder her, um den Status zu sehen.",scan_unavailable:"MQTT ist in Home Assistant nicht verfügbar oder wird nicht unterstützt.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · unvollständige Daten",status_no_links:"Empfangen: {time} · keine bestätigten Routen",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich",route_other_space_named:"{space} ({name})",route_device_not_on_plan_named:"Gerät ist nicht im Plan ({name})",route_unknown:"Keine Routendaten",route_stale:"Veraltete Daten",route_partial:"Unvollständige Daten",status_cache:"Integrationscache; Zeitpunkt des Funkscans unbekannt"};const n="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,n as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";var e={title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Affiche les parents et prochains sauts signalés par l’intégration. Le survol ne lance aucune requête.",help:"Affiche le parent d’un appareil terminal ou le prochain saut actif d’un routeur vers le coordinateur selon l’intégration. Il y a au plus une flèche sortante ; les flèches entrantes indiquent les appareils passant par ce nœud. Les simples voisins sont masqués. Toutes les lignes sont continues. Ligne et flèche utilisent le LQI 0–255 : rouge à 0, jaune à 128, vert à 255. Sans LQI, elles sont grises avec un contour noir. Une étiquette au bout de la flèche nomme une cible dans un autre espace ou absente du plan. Aucune donnée de route indique l’absence d’information univoque, pas un appareil hors ligne. Il s’agit des derniers relevés de l’intégration, pas du trajet des paquets en direct ni d’une garantie du chemin entier. Le cache ZHA peut déjà être ancien à la lecture ; les données anciennes et incomplètes sont signalées. Les liens ne sont visibles qu’à la souris.",help_aria:"Aide : liens Zigbee",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau avec les routes peut prendre plus de 10 minutes. House Plan continue d’attendre en arrière-plan. L’analyse peut réduire temporairement la réactivité de Zigbee.",scan_connecting:"Connexion à Zigbee2MQTT",scan_waiting:"En attente de la carte de Zigbee2MQTT",scan_elapsed:"Temps écoulé : {time}",scan_background_hint:"Vous pouvez fermer les réglages — l’attente continuera en arrière-plan.",scan_long_wait:"Zigbee2MQTT n’a pas encore répondu. L’attente continue.",scan_cancel:"Annuler la demande",scan_cancel_hint:"Seule l’attente de House Plan est annulée. L’analyse de Zigbee2MQTT peut continuer.",scan_cancelled:"Attente annulée. L’analyse de Zigbee2MQTT n’est peut-être pas encore terminée.",scan_backend_required:"Mettez à jour l’intégration House Plan et redémarrez Home Assistant pour utiliser les analyses de carte en arrière-plan.",scan_connection_lost:"La connexion MQTT a été perdue ; le résultat n’est peut-être pas arrivé. L’attente est terminée, mais Zigbee2MQTT peut encore analyser le réseau.",scan_frontend_disconnected:"La connexion à Home Assistant a été interrompue. L’attente continue en arrière-plan. Reconnectez-vous pour retrouver l’état de la demande.",scan_unavailable:"MQTT n’est pas disponible ou n’est pas pris en charge dans Home Assistant.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · données incomplètes",status_no_links:"Reçu : {time} · aucune route confirmée",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace",route_other_space_named:"{space} ({name})",route_device_not_on_plan_named:"Appareil absent du plan ({name})",route_unknown:"Aucune donnée de route",route_stale:"Données anciennes",route_partial:"Données incomplètes",status_cache:"Cache de l’intégration ; date de l’analyse radio inconnue"};const n="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";export{e as dictionary,n as fingerprint};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";var e={title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",help:"Montre avec quels appareils Zigbee chaque appareil communique directement. La couleur de la ligne est la qualité du lien (LQI) sur la même échelle que l'indicateur LQI de l'appareil : rouge à 40 et en dessous, vert à 180 et au-dessus. Une ligne pointillée signifie que la qualité n'a pas été communiquée. Une flèche pointe vers l'appareil suivant sur le chemin du coordinateur : une flèche part, les flèches entrantes sont les appareils qui passent par celui-ci. Une ligne sans flèche est un voisin de secours. C'est l'arbre de routes que construit House Plan, pas le trajet d'un paquet à cet instant : une étiquette au bout d'une flèche signifie que sa cible n'est pas sur ce plan, et l'absence de flèche que la route est inconnue. Les données proviennent du dernier chargement et vieillissent ; les liens ne sont visibles qu'à la souris.",help_aria:"Aide : liens Zigbee",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau peut durer de 10 secondes à 2 minutes et réduire temporairement la réactivité de Zigbee.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · certains nœuds sont omis",status_no_links:"Reçu : {time} · aucun lien observé trouvé",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace"};const s="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";export{e as dictionary,s as fingerprint};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";var e={title:"Связи Zigbee",toggle:"Показывать связи Zigbee при наведении на устройство",hint:"Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",help:"Показывает, с какими устройствами Zigbee каждое связано напрямую. Цвет линии — качество связи (LQI) по той же шкале, что у показателя LQI устройства: красный при 40 и ниже, зелёный при 180 и выше. Пунктир — качество не сообщено. Стрелка ведёт к следующему устройству по пути к координатору: исходящая одна, входящие — те, кто ходит через это устройство. Линия без стрелки — запасной сосед. Это дерево маршрутов, которое строит House Plan, а не путь пакета в эту секунду: подпись на конце стрелки значит, что цель не на этом плане, а отсутствие стрелки — что путь неизвестен. Данные из последней загрузки и устаревают, связи видны только при работе мышью.",help_aria:"Справка: связи Zigbee",admin_only:"Данные топологии доступны только администраторам Home Assistant.",save_first:"Сохраните настройку перед загрузкой данных топологии.",zha:"ZHA",zha_read:"Прочитать данные ZHA",zha_hint:"Читает сохранённую топологию ZHA и не запускает радио-сканирование.",z2m:"Zigbee2MQTT",z2m_topics:"Базовые топики (по одному в строке)",z2m_update:"Обновить карту",z2m_warning:"Сканирование карты сети занимает от 10 секунд до 2 минут и временно может снизить отзывчивость Zigbee.",status_idle:"Данные не загружены",status_loading:"Загрузка…",status_ready:"Получено: {time}",status_stale:"Получено: {time} · данные устарели",status_partial:"Получено: {time} · часть узлов не показана",status_no_links:"Получено: {time} · наблюдаемые связи не найдены",error_permission:"Нужны права администратора.",error_unsupported:"Провайдер или API Home Assistant недоступен.",error_timeout:"Провайдер не ответил вовремя.",error_invalid_topic:"Проверьте базовый топик Zigbee2MQTT.",error_invalid_payload:"Провайдер вернул неподдерживаемые данные топологии.",error_provider:"Не удалось загрузить данные топологии.",remote_count:"+{n} в других пространствах",route_device_not_on_plan:"устройства нет на плане",route_coordinator_not_on_plan:"координатора нет на плане",route_other_space:"другое пространство"};const t="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";export{e as dictionary,t as fingerprint};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";import{n as t,c as n}from"./zigbee-topology-BbetEPvj.js";import{aM as e}from"./houseplan-card-Ddo22-SB.js";import"./namespace-language-CwX4Ljv1.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__="d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96";try{await import("./houseplan-assets/houseplan-card-Ddo22-SB.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__="06f4c9f5768e50e86177cdd0ad3d4e55e568a796d9ba6db4d99a8ada55669698";try{await import("./houseplan-assets/houseplan-card-DNuV5X02.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
@@ -35,6 +35,7 @@ from .const import (
|
||||
PLANS_URL,
|
||||
VERSION,
|
||||
)
|
||||
from .led_strips import led_strip_link_report, plan_only_strips, unbind_strips
|
||||
from .radar_validation import validate_marker_radars
|
||||
from .store import HouseplanData
|
||||
from .validation import (
|
||||
@@ -294,11 +295,16 @@ def _project_plan_only_space(space: dict[str, Any]) -> dict[str, Any]:
|
||||
projected["decor"] = [
|
||||
_project_plan_only_decor(shape) for shape in space.get("decor") or []
|
||||
]
|
||||
led_strips = plan_only_strips(space)
|
||||
if led_strips is not None:
|
||||
# #780: plan-only keeps the strip geometry and never a device link.
|
||||
projected["led_strips"] = led_strips
|
||||
if "stairs" in space:
|
||||
projected["stairs"] = [
|
||||
_pick_fields(stair, (
|
||||
"id", "kind", "x", "y", "angle", "direction",
|
||||
"target_space_id", "length", "width", "radius",
|
||||
"color", "opacity", "fill_color", "fill_opacity",
|
||||
))
|
||||
for stair in space.get("stairs") or []
|
||||
]
|
||||
@@ -693,6 +699,12 @@ def parse_document(raw: bytes) -> dict[str, Any]:
|
||||
# before an import token can be issued.
|
||||
if model > 0:
|
||||
config_candidate["model_version"] = model
|
||||
# #780: the schema step unbinds strips whose marker is not in the
|
||||
# document; count them before it does, for the import summary.
|
||||
unbound_led_strips = (
|
||||
led_strip_link_report(config_candidate)["unbound"]
|
||||
if isinstance(config_candidate, dict) else 0
|
||||
)
|
||||
config = CONFIG_SCHEMA(config_candidate)
|
||||
config.pop("model_version", None)
|
||||
except (vol.Invalid, TypeError, ValueError) as err:
|
||||
@@ -729,6 +741,10 @@ def parse_document(raw: bytes) -> dict[str, Any]:
|
||||
**(document.get("transfer") or {}),
|
||||
"dropped_marker_links": dropped_marker_links,
|
||||
}
|
||||
# Computed here, never trusted from the file.
|
||||
document["transfer"].pop("unbound_led_strips", None)
|
||||
if unbound_led_strips:
|
||||
document["transfer"]["unbound_led_strips"] = unbound_led_strips
|
||||
if plan_only:
|
||||
_validate_plan_only_document(document, config, layout, placement)
|
||||
if len(json.dumps(
|
||||
@@ -1607,6 +1623,14 @@ def build_space_merge(
|
||||
marker.pop("value_source", None)
|
||||
dropped_marker_links += 1
|
||||
|
||||
# #780: an LED strip follows its marker through the same id remap; a
|
||||
# marker that was skipped or virtualised by the duplicate policy is not
|
||||
# transferred, so the strip arrives unbound (geometry kept, no device).
|
||||
unbound_led_strips = unbind_strips(space, remap={
|
||||
old_id: new_id for old_id, new_id in marker_map.items()
|
||||
if old_id not in virtualized_targets
|
||||
})
|
||||
|
||||
output_markers_by_id = {
|
||||
str(marker.get("id")): marker for marker in output_markers
|
||||
if marker.get("id") is not None
|
||||
@@ -1708,6 +1732,7 @@ def build_space_merge(
|
||||
"virtualized": virtualized,
|
||||
"orphan_markers": len(output_markers) - len(marker_map),
|
||||
"dropped_marker_links": dropped_marker_links,
|
||||
"unbound_led_strips": unbound_led_strips,
|
||||
"repaired_target_refs": repaired_target_refs,
|
||||
"preserved_unresolved_refs": sum(
|
||||
int(value) for value in reference_report["preservedUnresolved"].values()
|
||||
@@ -1913,6 +1938,14 @@ def _materialize_import_candidate(
|
||||
details = {
|
||||
"dropped_marker_links": _transfer_dropped_marker_links(prepared),
|
||||
}
|
||||
# #780: strips unbound by the document itself (counted while parsing)
|
||||
# plus those the transfer unbound (space merge) or the schema step will.
|
||||
transfer = prepared.get("transfer") if isinstance(prepared.get("transfer"), dict) else {}
|
||||
details["unbound_led_strips"] = (
|
||||
int(details.get("unbound_led_strips", 0))
|
||||
+ int(transfer.get("unbound_led_strips", 0) or 0)
|
||||
+ led_strip_link_report(config)["unbound"]
|
||||
)
|
||||
try:
|
||||
config = CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
|
||||
Executable
+315
@@ -0,0 +1,315 @@
|
||||
"""LED strips: stored shape, link normalisation and transfer rules (#780).
|
||||
|
||||
A strip is optional geometry of a space::
|
||||
|
||||
space.led_strips = [{"id": str, "points": [[x, y], ...],
|
||||
"marker": str | None, "active": bool (optional)}]
|
||||
|
||||
``active`` is the representation of the bound marker: absent/``True`` draws
|
||||
the strip, ``False`` keeps the shape hidden while the same marker is shown as
|
||||
an ordinary icon. It is neither ``marker.hidden`` nor the power state.
|
||||
|
||||
The module is pure (voluptuous only), so the schema, the write-path
|
||||
normalisation and the import/plan-only projections are exercised by the pure
|
||||
pytest suite without Home Assistant.
|
||||
|
||||
Write-path order (ТЗ #780 §9):
|
||||
|
||||
1. structure, types and limits (``LED_STRIPS_SCHEMA`` inside ``SPACE_SCHEMA``);
|
||||
2. per-space invariants and duplicate links — **before** any normalisation, so
|
||||
two strips pointing at the same missing id are still a conflict;
|
||||
3. normalisation: a link to a marker that is not live in the resulting config
|
||||
becomes an unbound strip (``marker: None, active: True``) with its id and
|
||||
points intact — a client that does not know about strips may delete a bound
|
||||
marker and its save must not fail; a live marker with an empty ``space``
|
||||
adopts the strip's space;
|
||||
4. referential check: a live marker with a non-empty foreign ``space``
|
||||
rejects the whole write.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import math
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
MAX_LED_STRIPS = 50
|
||||
MAX_LED_POINTS = 50
|
||||
LED_ID_MAX = 64
|
||||
LED_MARKER_MAX = 500
|
||||
CANVAS_LIMIT = 5000.0
|
||||
|
||||
LED_KEY = "led_strips"
|
||||
|
||||
|
||||
def _coordinate(value: Any) -> float:
|
||||
"""A stored coordinate: a real finite number, never a string or a bool."""
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise vol.Invalid("LED strip coordinate must be a number")
|
||||
number = float(value)
|
||||
if not math.isfinite(number):
|
||||
raise vol.Invalid("LED strip coordinate must be finite")
|
||||
if abs(number) > CANVAS_LIMIT:
|
||||
raise vol.Invalid("LED strip coordinate is outside the canvas")
|
||||
return number
|
||||
|
||||
|
||||
def _strict_bool(value: Any) -> bool:
|
||||
if not isinstance(value, bool):
|
||||
raise vol.Invalid("LED strip active must be a boolean")
|
||||
return value
|
||||
|
||||
|
||||
def _marker_ref(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str) or not value or len(value) > LED_MARKER_MAX:
|
||||
raise vol.Invalid("LED strip marker must be a non-empty string or null")
|
||||
return value
|
||||
|
||||
|
||||
def _strip_id(value: Any) -> str:
|
||||
if not isinstance(value, str) or not value or len(value) > LED_ID_MAX:
|
||||
raise vol.Invalid("LED strip id must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
_POINT = vol.All([_coordinate], vol.Length(min=2, max=2))
|
||||
|
||||
LED_STRIP_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("id"): _strip_id,
|
||||
vol.Required("points"): vol.All(
|
||||
list, vol.Length(min=2, max=MAX_LED_POINTS), [_POINT],
|
||||
),
|
||||
vol.Optional("marker", default=None): _marker_ref,
|
||||
vol.Optional("active"): _strict_bool,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
LED_STRIPS_SCHEMA = vol.All(list, vol.Length(max=MAX_LED_STRIPS), [LED_STRIP_SCHEMA])
|
||||
|
||||
|
||||
def _same(a: list[Any], b: list[Any]) -> bool:
|
||||
return float(a[0]) == float(b[0]) and float(a[1]) == float(b[1])
|
||||
|
||||
|
||||
def strip_is_closed(points: list[Any]) -> bool:
|
||||
return len(points) >= 4 and _same(points[0], points[-1])
|
||||
|
||||
|
||||
def polyline_length(points: list[Any]) -> float:
|
||||
total = 0.0
|
||||
for index in range(1, len(points)):
|
||||
ax, ay = points[index - 1]
|
||||
bx, by = points[index]
|
||||
total += math.hypot(float(bx) - float(ax), float(by) - float(ay))
|
||||
return total
|
||||
|
||||
|
||||
def _distinct_vertices(points: list[Any]) -> int:
|
||||
seen: set[tuple[float, float]] = set()
|
||||
for point in points:
|
||||
seen.add((float(point[0]), float(point[1])))
|
||||
return len(seen)
|
||||
|
||||
|
||||
def validate_strip_shape(strip: dict[str, Any]) -> None:
|
||||
"""Geometry invariants that the type schema cannot express."""
|
||||
points = strip["points"]
|
||||
if _distinct_vertices(points) < 2 or not polyline_length(points) > 0:
|
||||
raise vol.Invalid("LED strip needs at least two distinct points")
|
||||
if _same(points[0], points[-1]) and len(points) > 2:
|
||||
# A repeated first point closes the strip: three distinct vertices.
|
||||
if _distinct_vertices(points[:-1]) < 3:
|
||||
raise vol.Invalid("a closed LED strip needs three distinct vertices")
|
||||
if strip.get("active") is False and not isinstance(strip.get("marker"), str):
|
||||
raise vol.Invalid("a hidden LED strip shape must belong to a marker")
|
||||
|
||||
|
||||
class LedStripLinkError(ValueError):
|
||||
"""A write that cannot be normalised: duplicate or foreign link."""
|
||||
|
||||
code = "invalid_led_strip"
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
# Marker ids may carry user-controlled HA identifiers: a stable
|
||||
# message without the value, like DuplicateMarkerIdError.
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def _live_markers(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
live: dict[str, dict[str, Any]] = {}
|
||||
for marker in config.get("markers") or []:
|
||||
if not isinstance(marker, dict) or marker.get("removed") is True:
|
||||
continue
|
||||
marker_id = marker.get("id")
|
||||
if isinstance(marker_id, str) and marker_id and marker_id not in live:
|
||||
live[marker_id] = marker
|
||||
return live
|
||||
|
||||
|
||||
def _strips(config: dict[str, Any]) -> Iterator[tuple[dict[str, Any], dict[str, Any]]]:
|
||||
for space in config.get("spaces") or []:
|
||||
if not isinstance(space, dict):
|
||||
continue
|
||||
for strip in space.get(LED_KEY) or []:
|
||||
if isinstance(strip, dict):
|
||||
yield space, strip
|
||||
|
||||
|
||||
def preserve_led_strips(config: dict[str, Any], previous: dict[str, Any] | None) -> int:
|
||||
"""An ordinary writer that omits ``led_strips`` keeps the stored shapes (#780 r1 H1).
|
||||
|
||||
A previous frontend does not know the field and sends every space without
|
||||
it; the omission is not a deletion. A new client deletes shapes by sending
|
||||
an explicit list (``[]`` removes all of them). Applies to spaces that keep
|
||||
their id; a space the writer removed takes its strips with it. Runs on the
|
||||
ordinary write path before normalisation, so a preserved link to a marker
|
||||
the same write deleted becomes an unbound strip by the usual rule.
|
||||
Returns how many spaces received their stored shapes back.
|
||||
"""
|
||||
stored = {
|
||||
space.get("id"): space[LED_KEY]
|
||||
for space in (previous or {}).get("spaces") or []
|
||||
if isinstance(space, dict) and isinstance(space.get(LED_KEY), list) and space[LED_KEY]
|
||||
}
|
||||
restored = 0
|
||||
for space in config.get("spaces") or []:
|
||||
if not isinstance(space, dict) or LED_KEY in space:
|
||||
continue
|
||||
shapes = stored.get(space.get("id"))
|
||||
if shapes:
|
||||
space[LED_KEY] = copy.deepcopy(shapes)
|
||||
restored += 1
|
||||
return restored
|
||||
|
||||
|
||||
def led_strip_link_report(config: dict[str, Any]) -> dict[str, int]:
|
||||
"""What the normaliser would change, without changing anything.
|
||||
|
||||
The write path answers the client with these counters so a new client
|
||||
knows to re-read; an old client ignores the extra keys.
|
||||
"""
|
||||
live = _live_markers(config)
|
||||
unbound = adopted = 0
|
||||
for _space, strip in _strips(config):
|
||||
ref = strip.get("marker")
|
||||
if not isinstance(ref, str) or not ref:
|
||||
continue
|
||||
marker = live.get(ref)
|
||||
if marker is None:
|
||||
unbound += 1
|
||||
elif marker.get("space") in (None, ""):
|
||||
adopted += 1
|
||||
return {"unbound": unbound, "space_adopted": adopted}
|
||||
|
||||
|
||||
def normalize_led_strip_links(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate and normalise every strip of a whole configuration in place.
|
||||
|
||||
Runs after the type schema and the coordinate canonicalisation, so the
|
||||
shape invariants judge the stored numbers.
|
||||
"""
|
||||
taken: set[str] = set()
|
||||
for space in config.get("spaces") or []:
|
||||
if not isinstance(space, dict):
|
||||
continue
|
||||
ids: set[str] = set()
|
||||
for strip in space.get(LED_KEY) or []:
|
||||
if strip["id"] in ids:
|
||||
raise vol.Invalid("LED strip ids must be unique within a space")
|
||||
ids.add(strip["id"])
|
||||
validate_strip_shape(strip)
|
||||
ref = strip.get("marker")
|
||||
if isinstance(ref, str):
|
||||
if ref in taken:
|
||||
raise LedStripLinkError("a marker is bound to more than one LED strip")
|
||||
taken.add(ref)
|
||||
live = _live_markers(config)
|
||||
for space, strip in _strips(config):
|
||||
ref = strip.get("marker")
|
||||
if not isinstance(ref, str):
|
||||
strip["marker"] = None
|
||||
continue
|
||||
marker = live.get(ref)
|
||||
if marker is None:
|
||||
strip["marker"] = None
|
||||
strip["active"] = True
|
||||
continue
|
||||
owner = marker.get("space")
|
||||
if owner in (None, ""):
|
||||
marker["space"] = space["id"]
|
||||
elif owner != space["id"]:
|
||||
raise LedStripLinkError("an LED strip is bound to a marker of another space")
|
||||
return config
|
||||
|
||||
|
||||
def config_led_strip_links(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""voluptuous step for CONFIG_SCHEMA: ``LedStripLinkError`` → ``vol.Invalid``."""
|
||||
try:
|
||||
return normalize_led_strip_links(config)
|
||||
except LedStripLinkError as err:
|
||||
raise vol.Invalid(f"{LedStripLinkError.code}: {err.reason}") from err
|
||||
|
||||
|
||||
def unbind_strips(space: dict[str, Any], keep: set[str] | None = None,
|
||||
remap: dict[str, str] | None = None) -> int:
|
||||
"""Transfer rule for imports/copies: remap a link or make the strip unbound.
|
||||
|
||||
``remap`` maps the source marker id to the id it received in the target;
|
||||
a marker that was not transferred (skipped duplicate, plan-only, dropped)
|
||||
leaves an unbound strip with ``active: True``. Returns how many strips
|
||||
became unbound, for the import summary.
|
||||
"""
|
||||
unbound = 0
|
||||
for strip in space.get(LED_KEY) or []:
|
||||
if not isinstance(strip, dict):
|
||||
continue
|
||||
ref = strip.get("marker")
|
||||
if not isinstance(ref, str) or not ref:
|
||||
strip["marker"] = None
|
||||
continue
|
||||
target = (remap or {}).get(ref)
|
||||
if target is None and keep is not None and ref in keep:
|
||||
target = ref
|
||||
if target is None:
|
||||
strip["marker"] = None
|
||||
strip["active"] = True
|
||||
unbound += 1
|
||||
else:
|
||||
strip["marker"] = target
|
||||
return unbound
|
||||
|
||||
|
||||
def plan_only_strips(space: dict[str, Any]) -> list[dict[str, Any]] | None:
|
||||
"""Plan-only export: geometry stays, device links never leak."""
|
||||
if LED_KEY not in space:
|
||||
return None
|
||||
projected = []
|
||||
for strip in space.get(LED_KEY) or []:
|
||||
if not isinstance(strip, dict):
|
||||
continue
|
||||
projected.append({
|
||||
"id": strip.get("id"),
|
||||
"points": [list(point) for point in strip.get("points") or []],
|
||||
"marker": None,
|
||||
"active": True,
|
||||
})
|
||||
return projected
|
||||
|
||||
|
||||
def led_strip_counts(config: dict[str, Any]) -> dict[str, int]:
|
||||
"""Support-package counters: no coordinates, names or HA identifiers."""
|
||||
total = unbound = hidden = 0
|
||||
for _space, strip in _strips(config):
|
||||
total += 1
|
||||
if not isinstance(strip.get("marker"), str):
|
||||
unbound += 1
|
||||
elif strip.get("active") is False:
|
||||
hidden += 1
|
||||
return {"led_strips": total, "led_strips_unbound": unbound, "led_strips_hidden": hidden}
|
||||
@@ -2,7 +2,8 @@
|
||||
"domain": "houseplan",
|
||||
"name": "House Plan",
|
||||
"after_dependencies": [
|
||||
"lovelace"
|
||||
"lovelace",
|
||||
"mqtt"
|
||||
],
|
||||
"codeowners": [
|
||||
"@Matysh"
|
||||
@@ -20,5 +21,5 @@
|
||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.78.0-beta.5"
|
||||
"version": "1.80.0-beta.4"
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ class HouseplanData:
|
||||
junction_baseline: tuple[int, dict[str, dict[str, int]]] | None = None
|
||||
# Runtime-only #485 authority; never serialized by HouseplanStore.
|
||||
radar_coordinator: Any | None = None
|
||||
# #800: MQTT jobs and last-good maps live only for this loaded entry.
|
||||
zigbee_coordinator: Any | None = None
|
||||
|
||||
|
||||
HouseplanConfigEntry = ConfigEntry[HouseplanData]
|
||||
@@ -133,7 +135,7 @@ def create_data(hass: HomeAssistant) -> HouseplanData:
|
||||
)
|
||||
from .virtual_lights import VirtualLightController
|
||||
|
||||
data.virtual_lights = VirtualLightController(data.virtual_light_store)
|
||||
data.virtual_lights = VirtualLightController(hass, data.virtual_light_store)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from .const import (
|
||||
MAX_SUPPORT_ATTACHMENT_BYTES,
|
||||
PLAN_MODEL_VERSION,
|
||||
)
|
||||
from .led_strips import led_strip_counts
|
||||
|
||||
PACKAGE_FORMAT = "houseplan-support-package"
|
||||
PACKAGE_VERSION = 1
|
||||
@@ -158,6 +159,9 @@ def _global_settings(value: object) -> dict[str, Any]:
|
||||
volumetric_view = value.get("volumetric_view")
|
||||
if isinstance(volumetric_view, bool):
|
||||
out["volumetric_view"] = volumetric_view
|
||||
moon = value.get("moon")
|
||||
if isinstance(moon, bool):
|
||||
out["moon"] = moon
|
||||
fill_colors = value.get("fill_colors")
|
||||
if isinstance(fill_colors, dict):
|
||||
palette = {
|
||||
@@ -381,6 +385,7 @@ def _project_space(ids: _Pseudonyms, space: dict[str, Any], index: int) -> dict[
|
||||
projected = {"id": ids.get("stair", item.get("id"))}
|
||||
projected.update(_copy_keys(item, (
|
||||
"kind", "x", "y", "angle", "direction", "length", "width", "radius",
|
||||
"color", "opacity", "fill_color", "fill_opacity",
|
||||
)))
|
||||
if item.get("target_space_id"):
|
||||
projected["target_space_id"] = ids.get("space", item.get("target_space_id"))
|
||||
@@ -453,10 +458,21 @@ def _summary(config: object, layout: object) -> dict[str, Any]:
|
||||
"lifecycle": dict(sorted(lifecycles.items())),
|
||||
"binding": dict(sorted(bindings.items())),
|
||||
},
|
||||
# #780: counters only — no coordinates, room names or HA identifiers.
|
||||
"led_strips": _led_strip_summary(spaces),
|
||||
"layout_entries": len(layout) if isinstance(layout, dict) else 0,
|
||||
}
|
||||
|
||||
|
||||
def _led_strip_summary(spaces: list[Any]) -> dict[str, int]:
|
||||
counts = led_strip_counts({"spaces": [space for space in spaces if isinstance(space, dict)]})
|
||||
return {
|
||||
"total": counts["led_strips"],
|
||||
"unbound": counts["led_strips_unbound"],
|
||||
"hidden": counts["led_strips_hidden"],
|
||||
}
|
||||
|
||||
|
||||
def _binding_kind(value: object) -> str:
|
||||
text = str(value or "")
|
||||
if text == "virtual":
|
||||
|
||||
@@ -17,6 +17,11 @@ from custom_components.houseplan.coordinate_canonicalization import (
|
||||
canonicalize_layout_geometry,
|
||||
canonicalize_position,
|
||||
)
|
||||
from custom_components.houseplan.led_strips import (
|
||||
LED_STRIPS_SCHEMA,
|
||||
config_led_strip_links,
|
||||
preserve_led_strips,
|
||||
)
|
||||
from custom_components.houseplan.vacuum_routes import validate_marker_routes
|
||||
|
||||
# ---------- limits and extension sets ----------
|
||||
@@ -1208,7 +1213,7 @@ _LEGACY_MAX_ROOM_DRAFTS = 200
|
||||
_LEGACY_MAX_DRAFT_SEGMENTS = 2000
|
||||
MAX_PARTITIONS = 2000
|
||||
MAX_WALL_COLUMNS = 500
|
||||
# Open (virtual) wall stretches, docs/superpowers/specs/2026-08-05-open-spans-delete-design.md.
|
||||
# Open (virtual) wall stretches, legacy/docs/superpowers/specs/2026-08-05-open-spans-delete-design.md.
|
||||
# Every span is a piece of a shared boundary, so there can never be more of
|
||||
# them than there are wall segments — the cap is the walls' one (AUD-159B6-03).
|
||||
MAX_OPEN_SPANS = 500
|
||||
@@ -1280,7 +1285,7 @@ _GEOM = vol.All(_finite, vol.Range(min=-CANVAS_LIMIT, max=CANVAS_LIMIT))
|
||||
# than the old unit square — while staying strictly positive.
|
||||
_EXTENT = vol.All(_finite, vol.Range(min=0.001, max=CANVAS_LIMIT))
|
||||
|
||||
# The backdrop's uniform scale (docs/BACKDROP.md). A MULTIPLIER, not a
|
||||
# The backdrop's uniform scale (docs/DECOR-EDITOR.md §3). A MULTIPLIER, not a
|
||||
# coordinate: strictly positive, and bounded by what a person could mean —
|
||||
# a hundredth of the canvas is already a thumbnail, a hundred canvases is
|
||||
# already absurd. Mirrored by PLAN_SCALE_MIN/MAX in src/space-geometry.ts.
|
||||
@@ -1428,7 +1433,7 @@ SPACE_DISPLAY_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
# Live text on a decor label (docs/LIVE-TEXT.md). An entity id is
|
||||
# Live text on a decor label (docs/DECOR-EDITOR.md §5). An entity id is
|
||||
# `<domain>.<object_id>`; HA itself allows only lowercase letters, digits and
|
||||
# underscores in both halves. The bound is a sanity limit, not a policy.
|
||||
MAX_ENTITY_ID = 255
|
||||
@@ -1490,7 +1495,7 @@ DECOR_SCHEMA = vol.Any(
|
||||
vol.Schema({**_DECOR_COMMON, vol.Required("kind"): "text",
|
||||
vol.Required("x"): _NORM, vol.Required("y"): _NORM,
|
||||
# the template: newlines are the user's own line breaks and are
|
||||
# kept verbatim (docs/LIVE-TEXT.md); the label never wraps itself
|
||||
# kept verbatim (docs/DECOR-EDITOR.md §5); the label never wraps itself
|
||||
vol.Required("text"): vol.All(str, vol.Length(min=1, max=MAX_DECOR_TEXT)),
|
||||
# legacy font size ('s'|'m'|'l'). The dialog no longer offers it
|
||||
# — the block is scaled by its corner handles — but a plan
|
||||
@@ -1576,6 +1581,10 @@ STAIR_SCHEMA = vol.All(
|
||||
vol.Optional("length"): _FURN_SIZE,
|
||||
vol.Optional("width"): _FURN_SIZE,
|
||||
vol.Optional("radius"): _FURN_SIZE,
|
||||
vol.Optional("color"): _COLOR,
|
||||
vol.Optional("opacity"): vol.All(_finite, vol.Range(min=0.0, max=1.0)),
|
||||
vol.Optional("fill_color"): _COLOR,
|
||||
vol.Optional("fill_opacity"): vol.All(_finite, vol.Range(min=0.0, max=1.0)),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
@@ -1795,7 +1804,7 @@ SPACE_SCHEMA = vol.All(vol.Schema(
|
||||
vol.Optional("plan_aspect"): vol.Any(
|
||||
None, vol.All(vol.Coerce(float), vol.Range(min=0.05, max=20))
|
||||
),
|
||||
# Backdrop placement (docs/BACKDROP.md): the picture may be moved,
|
||||
# Backdrop placement (docs/DECOR-EDITOR.md §3): the picture may be moved,
|
||||
# resized per axis and rotated. Every transform field is optional; its
|
||||
# complete absence is the pre-v1.58.0 behaviour exactly, and an old
|
||||
# config validates unchanged. The offset is a normalised
|
||||
@@ -1873,6 +1882,8 @@ SPACE_SCHEMA = vol.All(vol.Schema(
|
||||
# (HP-1454-05): relying on a modern client to strip an unbounded legacy
|
||||
# list is not a limit, it is a hope. `Remove` returns the key stripped.
|
||||
vol.Remove("segments"): object,
|
||||
# #780: optional LED strip shapes of the space (led_strips.py).
|
||||
vol.Optional("led_strips"): LED_STRIPS_SCHEMA,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
), _space_geometry_invariants)
|
||||
@@ -2264,13 +2275,16 @@ def prepare_ordinary_summary_candidate(
|
||||
readable_entity_ids: set[str],
|
||||
normalize,
|
||||
) -> dict:
|
||||
"""Apply the one summary-panel contract shared by ordinary config writers.
|
||||
"""Apply the one preservation contract shared by ordinary config writers.
|
||||
|
||||
`normalize` owns the writer-specific schema/migration sequence. Preservation
|
||||
must happen before it; change-aware reference validation must happen after
|
||||
it. Authoritative full import/restore intentionally does not use this helper.
|
||||
Preserved: the summary panel namespace (#437) and the LED strip shapes of a
|
||||
space an older writer sent without the field (#780).
|
||||
"""
|
||||
preserve_summary_panel_namespace(candidate, previous)
|
||||
preserve_led_strips(candidate, previous)
|
||||
checked = normalize(candidate)
|
||||
validate_summary_panel_references(checked, previous, readable_entity_ids)
|
||||
return checked
|
||||
@@ -2309,6 +2323,8 @@ CONFIG_SCHEMA = vol.All(
|
||||
vol.Optional("show_room_tooltip"): bool,
|
||||
# #649: installation-wide 2.5D View; absent/false keeps Flat.
|
||||
vol.Optional("volumetric_view"): bool,
|
||||
# #661: the moon on the "Follow the Sun" background; absent/false = off.
|
||||
vol.Optional("moon"): bool,
|
||||
vol.Optional("summary_panel"): SUMMARY_PANEL_WIRE_SCHEMA,
|
||||
vol.Optional("radar"): vol.Schema({}, extra=vol.ALLOW_EXTRA),
|
||||
# Removed from the UI/runtime in 2026-08-08. Keep accepting the
|
||||
@@ -2347,4 +2363,7 @@ CONFIG_SCHEMA = vol.All(
|
||||
),
|
||||
canonicalize_config_geometry,
|
||||
_config_wall_segment_invariants,
|
||||
# #780: after canonicalisation — shape invariants judge stored numbers;
|
||||
# orphan links become unbound strips, foreign/duplicate links reject.
|
||||
config_led_strip_links,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .store import HouseplanStore
|
||||
|
||||
|
||||
@@ -81,12 +83,14 @@ def _snapshot_payload(
|
||||
class VirtualLightController:
|
||||
"""Runtime cache with coalesced durable writes for rapid toggles."""
|
||||
|
||||
def __init__(self, store: HouseplanStore) -> None:
|
||||
def __init__(self, hass: HomeAssistant, store: HouseplanStore) -> None:
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self._state: dict[str, Any] | None = None
|
||||
self._dirty = False
|
||||
self._save_task: asyncio.Task[None] | None = None
|
||||
self._flush_event = asyncio.Event()
|
||||
self._flush_lock = asyncio.Lock()
|
||||
|
||||
async def async_snapshot(
|
||||
self, config: dict[str, Any], config_rev: int,
|
||||
@@ -127,7 +131,7 @@ class VirtualLightController:
|
||||
|
||||
def _schedule_save(self) -> None:
|
||||
if self._save_task is None or self._save_task.done():
|
||||
self._save_task = asyncio.create_task(self._delayed_save())
|
||||
self._save_task = self.hass.async_create_task(self._delayed_save())
|
||||
|
||||
async def _delayed_save(self) -> None:
|
||||
try:
|
||||
@@ -156,16 +160,17 @@ class VirtualLightController:
|
||||
|
||||
async def async_flush(self) -> None:
|
||||
"""Persist the latest state before config transitions or unload."""
|
||||
task = self._save_task
|
||||
if task is not None:
|
||||
self._flush_event.set()
|
||||
await task
|
||||
if self._dirty and self._state is not None:
|
||||
payload = copy.deepcopy(self._state)
|
||||
await self.store.async_save(payload)
|
||||
self._dirty = False
|
||||
if not self._dirty:
|
||||
self._flush_event.clear()
|
||||
async with self._flush_lock:
|
||||
task = self._save_task
|
||||
if task is not None:
|
||||
self._flush_event.set()
|
||||
await task
|
||||
if self._dirty and self._state is not None:
|
||||
payload = copy.deepcopy(self._state)
|
||||
await self.store.async_save(payload)
|
||||
self._dirty = False
|
||||
if not self._dirty:
|
||||
self._flush_event.clear()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Forget cache after an external reconciliation wrote the store."""
|
||||
|
||||
@@ -70,6 +70,7 @@ from .import_export import (
|
||||
revalidate_candidate,
|
||||
)
|
||||
from .junction_limits import JunctionLimitError, validate_junction_limits
|
||||
from .led_strips import led_strip_link_report, preserve_led_strips
|
||||
from .plans import (
|
||||
QuotaError,
|
||||
collect_attachments,
|
||||
@@ -257,6 +258,7 @@ async def _resolved_write_pair(
|
||||
def async_register(hass: HomeAssistant) -> None:
|
||||
"""Register the WS commands."""
|
||||
from .radar_websocket import async_register as async_register_radar
|
||||
from .zigbee_websocket import async_register as async_register_zigbee
|
||||
|
||||
websocket_api.async_register_command(hass, ws_layout_get)
|
||||
websocket_api.async_register_command(hass, ws_trail_get)
|
||||
@@ -287,6 +289,7 @@ def async_register(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_support_preview_discard)
|
||||
websocket_api.async_register_command(hass, ws_support_submit)
|
||||
async_register_radar(hass)
|
||||
async_register_zigbee(hass)
|
||||
|
||||
|
||||
def _runtime(hass: HomeAssistant, connection, msg_id: int) -> HouseplanData | None:
|
||||
@@ -1480,6 +1483,8 @@ async def ws_config_get(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
|
||||
"decor_assets_api": DECOR_ASSETS_API_VERSION,
|
||||
"summary_panel_api": SUMMARY_PANEL_API_VERSION,
|
||||
**({"radar_stage1_api": 1} if rt.radar_coordinator is not None else {}),
|
||||
**({"zigbee_scan_api": 1} if rt.zigbee_coordinator is not None
|
||||
and not rt.zigbee_coordinator.closed else {}),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1681,6 +1686,14 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
|
||||
|
||||
radar_registry = radar_registry_evidence(hass)
|
||||
|
||||
# #780: what the write path will normalise in LED strip links (an
|
||||
# orphan becomes unbound, an empty marker.space adopts the strip's
|
||||
# space). Reported back so a new client re-reads; old ones ignore it.
|
||||
# An older writer omits the field: its stored shapes are kept first
|
||||
# (r1 H1), so the report also counts their links.
|
||||
preserve_led_strips(msg["config"], data.get("config"))
|
||||
led_report = led_strip_link_report(msg["config"])
|
||||
|
||||
def _validate_config_cpu():
|
||||
def _normalize(candidate):
|
||||
validate_wall_model_transition(candidate, data.get("config"))
|
||||
@@ -1789,7 +1802,10 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
|
||||
from .repairs import async_check_plan_files
|
||||
|
||||
hass.async_create_task(async_check_plan_files(hass, entry))
|
||||
connection.send_result(msg["id"], {"ok": True, "rev": new_rev})
|
||||
result: dict[str, Any] = {"ok": True, "rev": new_rev}
|
||||
if any(led_report.values()):
|
||||
result["led_strips"] = led_report
|
||||
connection.send_result(msg["id"], result)
|
||||
|
||||
|
||||
# ---------------- whole-plan maintenance ----------------
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user