Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -52,7 +52,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 }}
|
||||
@@ -112,7 +113,7 @@ jobs:
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
|
||||
needs: material
|
||||
if: needs.material.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -208,7 +209,8 @@ jobs:
|
||||
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:
|
||||
@@ -239,7 +241,8 @@ jobs:
|
||||
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 +278,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
|
||||
|
||||
@@ -32,9 +32,29 @@ permissions:
|
||||
jobs:
|
||||
dispatch:
|
||||
name: "Запустить Validate на dev с полным набором и дождаться результата"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
# #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
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
|
||||
@@ -25,7 +25,7 @@ permissions:
|
||||
jobs:
|
||||
metrics:
|
||||
name: "Снимок недели: issue, раунды, прогоны"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
# Код — из dev, как у reconcile: расписание читается из main, а исполняется
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -48,7 +48,8 @@ jobs:
|
||||
# Только статусные метки этапов ревью запускают конвейер (#499). Остальные
|
||||
# события помечаются skipped и не занимают место в группе concurrency.
|
||||
if: github.event.label.name == 'S4-spec-review' || github.event.label.name == 'S7-code-review'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
concurrency:
|
||||
group: process-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
@@ -282,7 +283,7 @@ jobs:
|
||||
issues: write
|
||||
needs: guard
|
||||
if: needs.guard.outputs.stage != ''
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: process-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
@@ -486,7 +487,7 @@ jobs:
|
||||
git fetch -q origin "+refs/heads/$BRANCH:refs/remotes/origin/$BRANCH"
|
||||
short_before=$(git rev-parse --short "$before")
|
||||
short_after=$(git rev-parse --short HEAD)
|
||||
echo "note=Ветка приведена к dev конвейером до ревью: поверх легло $behind коммит(ов) dev, $short_before -> $short_after. После ребейза это другой код (§7.2) — разбор полный, а не по дельте." >> "$GITHUB_OUTPUT"
|
||||
echo "note=Ветка приведена к dev конвейером до ревью: поверх легло $behind коммит(ов) dev, $short_before -> $short_after. После ребейза это другой код (§2.10) — разбор полный, а не по дельте." >> "$GITHUB_OUTPUT"
|
||||
echo "ветка $BRANCH приведена к dev: $short_before -> $short_after"
|
||||
|
||||
# Материал ревью — конкретный SHA (#312). Вердикт применим только к
|
||||
@@ -852,7 +853,7 @@ jobs:
|
||||
issues: write
|
||||
needs: [guard, prepare]
|
||||
if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: process-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
@@ -934,7 +935,7 @@ jobs:
|
||||
- name: Установить Chromium
|
||||
if: steps.pw.outputs.cache-hit != 'true'
|
||||
# Без --with-deps: системные библиотеки Chromium предустановлены в
|
||||
# образе ubuntu-latest, а apt при промахе кэша съедал минуты из бюджета
|
||||
# образе раннера, а apt при промахе кэша съедал минуты из бюджета
|
||||
# ревью и подолгу перебирал недоступное azure-зеркало (#175). Если
|
||||
# библиотека когда-нибудь пропадёт из образа, Chromium не запустится с
|
||||
# внятной ошибкой — тогда флаг вернуть.
|
||||
@@ -1014,13 +1015,18 @@ jobs:
|
||||
файла в материале нет — читай PROCESS.md §2.4, §2.7, §2.10,
|
||||
§4, §7.2, §8, §12. Задача правит сам конвейер, гейты или
|
||||
процесс — PROCESS.md целиком, §10 в первую очередь.
|
||||
3. AGENTS.md — классы изменений, трейлеры, гейты, формат вердикта.
|
||||
3. AGENTS.md — карта пакета, правило №1, классы изменений, треки,
|
||||
трейлеры и ожидание вердикта; сами правила — по его ссылкам.
|
||||
4. Тело issue #${{ github.event.issue.number }} и все комментарии.
|
||||
5. Если меняется видимое поведение — docs/USER-GUIDE.ru.md:
|
||||
терминология интерфейса берётся оттуда, а не изобретается.
|
||||
6. Канонический документ затронутой подсистемы: docs/SUN.md,
|
||||
LIGHT.md, CANVAS.md, WALL-THICKNESS.md, UX-MODES.md,
|
||||
CONFIG-COMPATIBILITY.md, TOUCH-SUPPORT.md.
|
||||
6. Канонический документ затронутой подсистемы (тот же список,
|
||||
что в AGENTS.md «Read this first»): docs/SUN.md, LIGHT.md,
|
||||
CANVAS.md, WALL-THICKNESS.md, UX-MODES.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.
|
||||
|
||||
**Если цикл не первый — объём разбора по дельте, а не заново**
|
||||
(PROCESS.md §2.10, issue #214): найди вердикт и материал
|
||||
@@ -1196,7 +1202,7 @@ jobs:
|
||||
issues: write
|
||||
needs: [guard, prepare, model_review]
|
||||
if: always() && needs.guard.outputs.stage != ''
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
group: process-issue-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
@@ -1533,7 +1539,7 @@ jobs:
|
||||
# Ревью идёт десятки минут, а dev за это время двигается (28 августа —
|
||||
# четыре раза за день). Вердикт при этом вынесен по дереву, которое уже не
|
||||
# совпадает с вершиной линии, и слияние приведёт ветку к dev — то есть в
|
||||
# dev уедет код, отличный от прочитанного (§7.2). Молчать об этом нельзя,
|
||||
# dev уедет код, отличный от прочитанного (§2.10). Молчать об этом нельзя,
|
||||
# но и шуметь на каждом прогоне ни к чему: строка появляется только когда
|
||||
# dev действительно ушёл и вердикт зелёный, то есть слияние вот-вот
|
||||
# случится (#364).
|
||||
@@ -1552,7 +1558,7 @@ jobs:
|
||||
if [ "$moved" -eq 0 ] || [ "$GREEN" != "true" ]; then exit 0; fi
|
||||
short=$(git rev-parse --short "$MATERIAL")
|
||||
gh issue comment "$NUM" --repo "${{ github.repository }}" --body \
|
||||
"Пока шло ревью, \`dev\` продвинулся на $moved коммит(ов). Материал ревью — \`$short\`. Вердикт вынесен по дереву, которое уже не совпадает с вершиной линии: слияние приведёт ветку к dev, и это другой код (§7.2)."
|
||||
"Пока шло ревью, \`dev\` продвинулся на $moved коммит(ов). Материал ревью — \`$short\`. Вердикт вынесен по дереву, которое уже не совпадает с вершиной линии: слияние приведёт ветку к dev, и это другой код (§2.10)."
|
||||
|
||||
# S8-merged утверждает, что код в dev. Значит слияние обязано произойти
|
||||
# ДО метки, иначе она врёт в промежутке.
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -122,7 +124,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 +292,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
|
||||
@@ -110,7 +110,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 превращает владелец.
|
||||
@@ -207,7 +207,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 +258,7 @@ jobs:
|
||||
publish:
|
||||
name: "Ревью релиза: документ в dev"
|
||||
needs: [prepare, model_review]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -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
|
||||
@@ -164,7 +165,10 @@ jobs:
|
||||
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 +238,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 +379,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 +480,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
|
||||
|
||||
@@ -50,7 +50,8 @@ jobs:
|
||||
# действовать — на уровне шагов, с той же гранулярностью в логе.
|
||||
preflight:
|
||||
name: "Предполёт: документация, провенанс, процесс"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
# `actions: read` — чтение списка прогонов Validate (#388), `issues: read` —
|
||||
# проверка 8 процессного гейта. Права перечислены явно, потому что job
|
||||
# обращается к API сверх содержимого репозитория.
|
||||
@@ -247,7 +248,8 @@ jobs:
|
||||
# там делает код-ревью, которое гоняет гейты само (#127).
|
||||
changes:
|
||||
name: Классификация изменённых файлов
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
# Чтение списка прогонов Validate — единственное, что этой job нужно сверх
|
||||
# содержимого репозитория (#387).
|
||||
permissions:
|
||||
@@ -406,7 +408,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 +558,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 +571,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
|
||||
@@ -598,7 +603,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 +732,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 +843,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 +873,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 +980,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 +1005,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 +1108,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 +1204,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 +1239,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 +1326,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,23 +4,26 @@ 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:
|
||||
@@ -32,61 +35,18 @@ route and `test/entry-cost.test.mjs` keeps this list equal to its routes:
|
||||
- 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
|
||||
|
||||
@@ -103,183 +63,100 @@ The label must be one of `S5-ready`, `S6-in-progress`, `S7-code-review`. Anythin
|
||||
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.
|
||||
|
||||
## 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): `small` is the default — the spec lives in the
|
||||
issue body and its review is a comment; taking the full track means naming the
|
||||
`small` criterion the task fails. `trivial` skips the spec stage for a bug whose
|
||||
expected behaviour is already on record. An **infrastructure** task — not a single
|
||||
class A file — skips analysis and spec and enters at `S7-code-review`
|
||||
(`PROCESS.md` §1). Code review is never skipped on any track: it 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
|
||||
carries **terminal** trailers:
|
||||
|
||||
```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, what the status permits, the branch against `dev`, 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 |
|
||||
|---|---|---|
|
||||
@@ -290,358 +167,21 @@ What the new label means:
|
||||
| `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`.
|
||||
|
||||
@@ -58,6 +58,19 @@ 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 by the
|
||||
`Docs screenshots` workflow (`demo/docs/capture.mjs` on the pinned Chromium)
|
||||
and accepted locally with `npm run docs:accept -- --reviewed --from=<unpacked
|
||||
artifact>`; when a change cannot move a pixel, `npm run docs:accept --
|
||||
--identical` re-captures locally, compares decoded pixels and refreshes only the
|
||||
source fingerprint. 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
|
||||
@@ -88,16 +101,13 @@ npm install # also installs .githooks through the prepare script
|
||||
|
||||
### 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
|
||||
@@ -130,8 +140,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.
|
||||
|
||||
@@ -215,9 +215,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
- **Скоуп не расширяется.** Найденное по пути становится новым issue в «Новое».
|
||||
Если находка блокирует — текущий issue уходит в «Заблокировано» со ссылкой.
|
||||
Попутных правок «раз уж я здесь» не бывает.
|
||||
- **Документация — в том же коммите,** что и поведение (действующая политика
|
||||
`docs/STATUS.md`): changelog RU+EN для пользовательского, `STATUS.md` для
|
||||
состояния, `DEVELOPMENT.md` для новых грабель, `ARCHITECTURE.md` для дизайна.
|
||||
- **Документация — в том же коммите,** что и поведение: changelog RU+EN для
|
||||
пользовательского, `STATUS.md` для состояния, `DEVELOPMENT.md` для новых
|
||||
грабель, `ARCHITECTURE.md` для дизайна.
|
||||
- **Выход:** локальный гейт зелёный (§8), хендофф-комментарий (§7.2).
|
||||
|
||||
### 2.7 Код-ревью
|
||||
@@ -251,6 +251,15 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов
|
||||
достаточно прогона со снятой защитой, приведённого в документе.
|
||||
|
||||
Новый мутант с browser-smoke guard допустим только когда инвариант нельзя
|
||||
доказать без браузера: `because` обязан назвать конкретную зависимость от
|
||||
DOM/CSS paint, измеренной геометрии, trusted pointer/lifecycle или browser
|
||||
wall-time, а id — попасть в размеченный реестр
|
||||
`docs/testing-notes/mutation-browser-guards.md`. `mutation-gate --check`
|
||||
показывает число browser guards против лимита 200, краснеет при росте выше
|
||||
лимита и предупреждает о любом id без browser-обоснования; ревьюер проверяет
|
||||
не только наличие строки, но и невозможность более дешёвого `node --test`.
|
||||
|
||||
Считаются **защитные AC без названного свидетеля**, а не мутанты на
|
||||
подсистему: у #421 мутанты были, и дыра всё равно проехала. «Сколько мутантов
|
||||
принесла задача» остаётся признаком — у #423 их ноль, и именно у #423 нашёлся
|
||||
@@ -264,7 +273,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
видит разницу.
|
||||
- **High блокируют.** Medium **в скоупе задачи** чинится в текущем issue:
|
||||
без High это жёлтый вердикт и возврат автору, фикс проходит повторный цикл.
|
||||
Medium **вне скоупа** — отдельный issue (#202).
|
||||
Medium **вне скоупа** — отдельный issue (#202). Жёлтый вердикт законен и
|
||||
тогда, когда все AC выполнены, если изменение не решает заявленный сценарий
|
||||
или ухудшает соседний.
|
||||
- **Вердикт привязан к SHA (#312).** Все числа и факты отчёта сверяются с
|
||||
`git rev-parse HEAD` непосредственно перед подведением итогов, а не с SHA,
|
||||
зафиксированным в начале разбора: во время ревью в ветку может прилететь
|
||||
@@ -277,8 +288,8 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
вызовом в `test-build`, а не поиском строки в исходнике: текстовый якорь
|
||||
краснеет на переносе метода без единой регрессии, и это делает вынос дороже,
|
||||
чем оставить монолит как есть. Список тестов, читающих монолит как текст,
|
||||
заморожен (`test/monolith-text-anchors.test.mjs`, 54 файла на 23.09.2026) и
|
||||
может только уменьшаться; новое имя в нём — находка ревью, а не запись в
|
||||
заморожен (`FROZEN_TEXT_ANCHOR_TESTS` в `test/monolith-text-anchors.test.mjs`)
|
||||
и может только уменьшаться; новое имя в нём — находка ревью, а не запись в
|
||||
список. Связность монолита измеряется шестью числами
|
||||
(`scripts/monolith-metrics.mjs`: делегаты, члены порта, `host.`, приватные
|
||||
члены порта и харнесса, байты `dist/`), база — `scripts/monolith-baseline.json`;
|
||||
@@ -349,10 +360,10 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
|
||||
|
||||
Находкой остаётся другое: **SHA, мёртвый уже в момент публикации отчёта** —
|
||||
он означает, что значение сняли до `amend` или `rebase` и не сверили перед
|
||||
выводом, как требует §7.2. Это отличие не теоретическое: на #403 оба
|
||||
источника, автор и ревьюер, независимо назвали один и тот же осиротевший
|
||||
SHA, и следующий раунд восстанавливал коммит по содержимому диффа руками
|
||||
(issue #413). Конвейер теперь такую публикацию останавливает сам;
|
||||
выводом, как требует §2.7 («Вердикт привязан к SHA»). Это отличие не
|
||||
теоретическое: на #403 оба источника, автор и ревьюер, независимо назвали один
|
||||
и тот же осиротевший SHA, и следующий раунд восстанавливал коммит по
|
||||
содержимому диффа руками (issue #413). Конвейер теперь такую публикацию останавливает сам;
|
||||
3. по каждой находке предыдущего раунда показать, **чем именно она закрыта** —
|
||||
строкой кода или текста, а не заявлением автора;
|
||||
4. заново проверять только те AC, чьё доказательство дельта задевает;
|
||||
@@ -386,16 +397,28 @@ dev, ни при публикации документа код-ревью: ин
|
||||
**Хранение документов** (решение владельца 23.09, #635): в `docs/reviews/`
|
||||
лежат все раунды всех задач текущей линии — предыдущие раунды нужны ссылкам
|
||||
«Унаследовано из r<N−1>» и якорям материала. При стабильном релизе документы
|
||||
задач, вошедших в него (`RELEASE-MEMBERSHIP.json` линии), переносятся в
|
||||
`legacy/reviews/<vX.Y.Z>/` одним коммитом класса C; индекс пересобирается и
|
||||
перечисляет только живые. Перенос — часть чеклиста стабильного релиза, не
|
||||
отдельная задача.
|
||||
задач, вошедших в него, переносятся в `legacy/reviews/<vX.Y.Z>/` одним
|
||||
коммитом класса C; индекс пересобирается и перечисляет только живые. Перенос —
|
||||
часть чеклиста стабильного релиза, не отдельная задача, и делает его
|
||||
`node scripts/reviews-archive.mjs --through=vX.Y.Z` (без `--apply` — только
|
||||
план, #682). Членство — трейлеры `Issue: #NN` в диапазоне линии, как у
|
||||
манифеста беты и ревью линии (§11.5); метка не доказательство. Уточнения:
|
||||
задача с трейлером и после тега остаётся в `docs/reviews/` целиком — её раунды
|
||||
ещё продолжаются; задача с трейлерами в двух выпущенных линиях уезжает целиком
|
||||
в последнюю; закрытая без выпуска (как #522) уезжает с линией, где лёг её
|
||||
документ — коммит документа несёт трейлер; `RELEASE-REVIEW-vX.Y.Z.md` уходит в
|
||||
каталог своего тега, поэтому перенос делается после публикации ревью линии.
|
||||
Относительные ссылки в перенесённых документах и в соседях, которые на них
|
||||
ссылаются, инструмент переписывает сам; `--check-links` печатает оставшиеся
|
||||
битые.
|
||||
Перенос идёт при пустой очереди `S7-code-review`: ребейз чужой ветки иначе
|
||||
упрётся в перемещённый каталог.
|
||||
|
||||
Дешёвые гейты (`typecheck`, `test`, `build` с проверкой целостности сборки, `bundle-policy --verify`) гоняются в
|
||||
каждом раунде: код изменился, а стоят они минуты. Тяжёлые — по дельте (§10.2).
|
||||
|
||||
**Разбор остаётся полным**, если дельта не локальна: ребейз на ушедший вперёд
|
||||
`dev` (после ребейза это другой код, §7.2), смена контракта поведения, задета
|
||||
`dev` (после ребейза это другой код, §10.4), смена контракта поведения, задета
|
||||
новая подсистема, либо объём дельты сопоставим с исходной задачей.
|
||||
|
||||
Сокращается объём **разбора, а не строгость**: правка по замечанию способна
|
||||
@@ -690,16 +713,6 @@ issue #NN
|
||||
прогон показал, почему это неверно: жёлтый там означал, что AC описывает неверное
|
||||
изменение контракта — реализовать такое ТЗ значило бы сделать ошибку по инструкции.
|
||||
|
||||
### 7.3 Расхождения с текущим состоянием, которые надо закрыть
|
||||
|
||||
1. ✅ **Статус ТЗ дублировал статус issue.** Закрыто 2026-09-10 (#517): ТЗ живёт
|
||||
в теле issue, `docs/specs/` — архив, индекс с колонкой «Статус ТЗ» удалён
|
||||
вместе с самой таблицей. Статус задачи — только метка `S*`.
|
||||
2. **Ревью до релиза 1.62 живут вне репозитория.** Документы `CODE-REVIEW-*.md` и
|
||||
`SPEC-REVIEW-*.md` за прежний период лежат в папке владельца, и переносить их
|
||||
задним числом смысла нет: они описывают код, которого уже нет. Новые документы
|
||||
ревью кладёт в `docs/reviews/` сам конвейер, в ветку задачи.
|
||||
|
||||
---
|
||||
|
||||
## 8. Гейты
|
||||
@@ -725,10 +738,11 @@ npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \
|
||||
# если менялось одно из зеркал junction limits
|
||||
```
|
||||
|
||||
**Новый код не добавляет `any`** (#342). В `src/**` уже 1034 вхождения явного
|
||||
`any` в 49 файлах; перетипизировать это одним заходом — месяц риска ради нуля
|
||||
пользовательской ценности, поэтому долг снимается при плановом извлечении
|
||||
подсистем (#425, прежний #34), а не разовой заменой. Гейт `scripts/no-new-any.mjs` судит
|
||||
**Новый код не добавляет `any`** (#342). Явного `any` в `src/**` — сотни
|
||||
вхождений (`node scripts/no-new-any.mjs --total`); перетипизировать это одним
|
||||
заходом — месяц риска ради нуля пользовательской ценности, поэтому долг
|
||||
снимается при плановом извлечении подсистем (#425, прежний #34), а не разовой
|
||||
заменой. Гейт `scripts/no-new-any.mjs` судит
|
||||
**только добавленные строки**: существующий долг на нетронутой строке законен,
|
||||
правка строки со старым `any` — новая ответственность. Исключение объявляется на
|
||||
той же строке, `// any-ok: <конкретная причина>`; голый маркер и причины вида
|
||||
@@ -767,8 +781,8 @@ npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \
|
||||
фронтенда делает его устаревшим. Выборка «по diff и AC» здесь не работает — diff
|
||||
всегда попадает, и решать нечего. Цена пропуска измерена: скриншоты не
|
||||
пересняли в #230 и #234, и `dev` стоял с красным job `docs`, пока это не нашли
|
||||
при следующей задаче (#237). Пересъёмка — `npm run build && node
|
||||
demo/docs/capture.mjs`, коммит вместе с задачей.
|
||||
при следующей задаче (#237). Пересъёмка — по двум абзацам выше, коммит
|
||||
вместе с задачей.
|
||||
|
||||
**Перф-смок в Validate зависит от диффа** (#473). Два glow-профиля
|
||||
гоняются всегда; при правке `src/iso-*` добавляется `large-house-isometric-v1`,
|
||||
@@ -1071,7 +1085,7 @@ Matysh/houseplan-card/.github/workflows/_<имя>.yml@dev` с `secrets: inherit`
|
||||
мутантов не запрашивают** (#601, решение владельца 20.09): мутационный гейт
|
||||
проверяет тесты, а не продукт (#513), к бете каждая задача прогнана им дважды —
|
||||
на ревью и на слитом после ребейза кандидате, — а ночью идёт полный реестр
|
||||
(`mutation-gate.yml`, 01:00 UTC). Релизный гейт (#541) требует полного Validate,
|
||||
(`mutation-gate.yml`, 00:43 UTC). Релизный гейт (#541) требует полного Validate,
|
||||
но не mutant-jobs; для ревью и слияния шесть исполненных mutant-jobs остаются
|
||||
обязательными.
|
||||
|
||||
@@ -1134,7 +1148,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
- ветка уже содержит весь `dev` — ничего;
|
||||
- отстала и ребейзится чисто — ребейз, `push --force-with-lease`, ревью по
|
||||
приведённому состоянию. Факт ребейза передаётся в промпт, чтобы сработало
|
||||
правило §7.2 о полном разборе вместо дельты;
|
||||
правило §2.10 о полном разборе вместо дельты;
|
||||
- конфликт — возврат в `S6-in-progress` **до** запуска ревью. Цикл при этом не
|
||||
расходуется: код никто не читал, вердикта нет.
|
||||
|
||||
@@ -1156,7 +1170,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
- если `dev` не двигался — push с `--force-with-lease` на текущую вершину;
|
||||
- если двигался — ребейз (конфликт — `S6-in-progress`, как раньше), сравнение
|
||||
patch-id проверенного и получившегося диффа (различие — `S7-code-review`: вердикт
|
||||
к другому диффу не применим, §7.2), публикация кандидата в ветку задачи, запуск
|
||||
к другому диффу не применим, §2.10), публикация кандидата в ветку задачи, запуск
|
||||
Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом
|
||||
SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту
|
||||
вершину, поверх которой кандидат собран. Отклонённый lease — `dev` двинулся снова
|
||||
@@ -1206,7 +1220,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
повторно **без вызова модели**, если последний документ этапа несёт записанный
|
||||
конвейером вердикт `green` с High 0 и дерево материала не изменилось ни в одном
|
||||
файле вне `docs/reviews/**` (сравнивает `git diff` по содержимому). Ребейз, правка
|
||||
теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §7.2 не
|
||||
теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §2.10 не
|
||||
ослабляется, оно просто не касается дерева, которое уже читали.
|
||||
|
||||
Цикл считается **по этапу**: вердикт по ТЗ не расходует бюджет код-ревью. Раньше
|
||||
@@ -1227,7 +1241,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
- issue создан в **той же сессии до коммита**, метка `hotfix`;
|
||||
- ТЗ «как сделано» + раздел «почему нельзя было ждать»;
|
||||
- в течение 24 часов задача ретроспективно проходит код-ревью;
|
||||
- аварийность названа явно в релизном хендоффе (действующее правило `AGENTS.md`).
|
||||
- аварийность названа явно в релизном хендоффе.
|
||||
|
||||
### 11.3 Гигиена репозитория
|
||||
|
||||
@@ -1369,70 +1383,3 @@ Golden, браузерные смоки, performance и полный HA-харн
|
||||
|
||||
**Нарушение процесса — тоже issue** (метка `process`): если правило удалось
|
||||
нарушить незаметно, виновата проверка.
|
||||
|
||||
---
|
||||
|
||||
## 13. Внедрение
|
||||
|
||||
Состояние на 2026-08-13.
|
||||
|
||||
1. ✅ **Метки созданы, бэклог размечен.** У всех открытых issue владельца ровно
|
||||
одна `S*`-метка, инварианты чистые.
|
||||
2. ✅ **Колонка «Статус ТЗ» убрана** — вместе со всем индексом: `docs/specs/`
|
||||
стал архивом, ТЗ переехало в тело issue (#517, 2026-09-10). Перенос старых
|
||||
документов ревью в `docs/reviews/` отменён: они описывают код, которого уже нет.
|
||||
3. ✅ **Гейт написан** — `scripts/process-gate.mjs` плюс job в `validate.yml`,
|
||||
issue #105. Прошёл **вне** флоу как инфраструктурная задача (§1, issue #118), а
|
||||
не через ТЗ и ревью, как предполагала прежняя редакция этого пункта.
|
||||
4. ✅ **Долг ревью списан решением владельца.** Беты `beta.2`…`beta.10` сделаны по
|
||||
прежнему процессу и не пересматриваются. Точка отсчёта — релиз 1.62.0; отсчёт
|
||||
начинается с первой беты следующей линии.
|
||||
5. ⏳ Завести issue на находку «смок `visual_continuity` не умеет падать» — это
|
||||
ровно тот класс дефектов, который в процессе без ручного тестирования стоит
|
||||
дороже всего.
|
||||
6. ✅ `BACKLOG-2026-08-11.md` — разовый отчёт, решения живут в issue.
|
||||
7. ✅ `AGENTS.md` переписан целиком, шире блока §14.
|
||||
8. ✅ **Канон перенесён в репозиторий** (issue #112). До этого полный процесс жил
|
||||
только в папке владельца, а в репозитории лежал файл на 51 строку про трейлеры
|
||||
коммитов — из свежего клона канон не был виден вообще.
|
||||
9. ✅ **`pre-push` написан** (§10.1, issue #121). Блокирующая проверка на клиенте
|
||||
есть; обойти её можно только `--no-verify`, и тогда то же найдёт CI.
|
||||
|
||||
---
|
||||
|
||||
## 14. Блок для AGENTS.md
|
||||
|
||||
```markdown
|
||||
## Процесс: код только через issue
|
||||
|
||||
Изменение продуктового кода без issue запрещено. Код меняется только из статуса
|
||||
«Готово к разработке» или дальше. Полные правила, критерии статусов и гейты —
|
||||
`docs/PROCESS.md`, читать до начала работы.
|
||||
|
||||
Жизненный цикл (статус = метка issue): `S1-new` → `S2-analysis` → `S3-spec` →
|
||||
`S4-spec-review` → `S5-ready` → `S6-in-progress` → `S7-code-review` → `S8-merged`
|
||||
→ закрытие пачкой при выпуске беты. Оба ревью возвращают на правки не более 4
|
||||
циклов; пятый заход — разбор у владельца (разделить / отклонить / арбитраж).
|
||||
|
||||
Инфраструктурная задача (ни одного файла класса A) делается сразу любым агентом:
|
||||
без `S*` → `S7-code-review` ↔ `S6-in-progress` → `S8-merged`. ТЗ и ревью ТЗ нет,
|
||||
код-ревью обязательно.
|
||||
|
||||
Ревью запускается **само** от меток `S4-spec-review` и `S7-code-review`.
|
||||
Deterministic prerequisites, модель и интеграция имеют отдельные пределы 55/45/55
|
||||
минут; обычно стадии заканчиваются существенно раньше. Поставив такую метку, автор
|
||||
не заканчивает работу, а ждёт смены метки опросом и продолжает по тому, чем она
|
||||
стала.
|
||||
|
||||
- ветка `issue/<NN>-<slug>`, коммиты с трейлерами `Issue: #NN` и `User-Visible: yes|no`;
|
||||
- работаем прямыми коммитами в `dev`, без PR: блокирующий гейт — локальный
|
||||
`pre-push` (ставится автоматически через `npm ci`), CI — страховка. Force-push
|
||||
в `dev` запрещён;
|
||||
- автор ≠ ревьюер, ни для ТЗ, ни для кода;
|
||||
- фазы ручного тестирования нет: автотесты пишутся в реализации, AC проверяет
|
||||
код-ревью, найденные позже дефекты — новые issue типа «баг»;
|
||||
- мелкие задачи (метка `small`, сложность ≤3): ТЗ в теле issue, ревью ТЗ
|
||||
комментарием, код-ревью — как обычно;
|
||||
- найденное вне скоупа — новый issue, а не попутная правка;
|
||||
- issue закрывает релиз-менеджер после выпуска беты, не исполнитель.
|
||||
```
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,30 @@ 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.
|
||||
"""
|
||||
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, {})
|
||||
@@ -252,6 +277,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 +301,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.78.0"
|
||||
DECOR_ASSETS_API_VERSION = 1
|
||||
SUMMARY_PANEL_API_VERSION = 1
|
||||
|
||||
|
||||
@@ -1,129 +1,129 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "d06c84d556e989bcf1e9fa8c0eb28b72de2cbf0fe7a8c35051d3497e78d9cf96",
|
||||
"fingerprint": "a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",
|
||||
"entry": "houseplan-card.js",
|
||||
"panelEntry": "houseplan-panel.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 299093,
|
||||
"initialViewGzipBytes": 300323,
|
||||
"initialPanelFiles": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-panel.js"
|
||||
],
|
||||
"initialPanelGzipBytes": 300614,
|
||||
"initialPanelGzipBytes": 301845,
|
||||
"initialPanelOnlyFiles": [
|
||||
"houseplan-panel.js"
|
||||
],
|
||||
"initialPanelOnlyGzipBytes": 2324,
|
||||
"initialPanelOnlyGzipBytes": 2325,
|
||||
"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-WDIF6iao.js",
|
||||
"houseplan-assets/de-E17v3gXt.js",
|
||||
"houseplan-assets/editor-CbeYCXUv.js",
|
||||
"houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/fr-CpDKyK4Z.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
|
||||
"houseplan-assets/iso-scene-render-BxXtKC5L.js",
|
||||
"houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js",
|
||||
"houseplan-assets/pdf-export-DL7VmwpB.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
|
||||
"houseplan-assets/zigbee-topology-DOAnbvlu.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
|
||||
],
|
||||
"lazyGzipBytes": 453435,
|
||||
"lazyGzipBytes": 456389,
|
||||
"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-WDIF6iao.js",
|
||||
"houseplan-assets/editor-CbeYCXUv.js",
|
||||
"houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js",
|
||||
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 235435,
|
||||
"lazyEditorGzipBytes": 238192,
|
||||
"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-WDIF6iao.js",
|
||||
"houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 27952,
|
||||
"lazyOnboardingGzipBytes": 27950,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-B2abA_bU.js",
|
||||
"houseplan-assets/fr-Bho0FfMD.js"
|
||||
"houseplan-assets/de-E17v3gXt.js",
|
||||
"houseplan-assets/fr-CpDKyK4Z.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 56230,
|
||||
"lazyLocaleGzipBytes": 56330,
|
||||
"lazyIsometricFiles": [
|
||||
"houseplan-assets/iso-scene-render-Soh6e8O4.js"
|
||||
"houseplan-assets/iso-scene-render-BxXtKC5L.js"
|
||||
],
|
||||
"lazyIsometricGzipBytes": 17885,
|
||||
"lazyFurnitureArtFiles": [
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js"
|
||||
],
|
||||
"lazyFurnitureArtGzipBytes": 16941,
|
||||
"lazyFurnitureArtGzipBytes": 16940,
|
||||
"lazyPdfFiles": [
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/pdf-export-De59asII.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"houseplan-assets/pdf-export-DL7VmwpB.js"
|
||||
],
|
||||
"lazyPdfGzipBytes": 126914,
|
||||
"lazyPdfGzipBytes": 126938,
|
||||
"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/settings-de-DfJ7EKF9.js",
|
||||
"houseplan-assets/settings-fr-Bl79E-6s.js",
|
||||
"houseplan-assets/settings-ru-CkNFmChi.js",
|
||||
"houseplan-assets/support-de-DZoi-fxk.js",
|
||||
"houseplan-assets/support-fr-Daosy89Q.js",
|
||||
"houseplan-assets/support-ru-e8-BrqtH.js",
|
||||
"houseplan-assets/topology-de-Cs36ouVE.js",
|
||||
"houseplan-assets/topology-fr-B8yoJGvc.js",
|
||||
"houseplan-assets/topology-ru-BRKShmrQ.js"
|
||||
],
|
||||
"lazyNamespaceLocaleGzipBytes": 21047,
|
||||
"lazyNamespaceLocaleGzipBytes": 21057,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-CXaZKwRy.js",
|
||||
"sha256": "001e8011f17ee8c1d3804304eb6734065b645a28b19beb8c18fa699f309de66c",
|
||||
"path": "houseplan-assets/backdrop-pick-WDIF6iao.js",
|
||||
"sha256": "5f6dcedf274594075d8acd31295d8c23f6d1408c890c086825db6bcbaa6c9b21",
|
||||
"rawBytes": 54017,
|
||||
"gzipBytes": 17577,
|
||||
"gzipBytes": 17576,
|
||||
"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/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/settings-de-BjfMuSw8.js",
|
||||
"houseplan-assets/settings-fr-BKCX3R6z.js",
|
||||
"houseplan-assets/settings-ru-D8xvcCw6.js"
|
||||
"houseplan-assets/settings-de-DfJ7EKF9.js",
|
||||
"houseplan-assets/settings-fr-Bl79E-6s.js",
|
||||
"houseplan-assets/settings-ru-CkNFmChi.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-B2abA_bU.js",
|
||||
"sha256": "63b70f53f23e194e16172c10b11dda1d19db0bebc23a5213444cab07da1e72be",
|
||||
"rawBytes": 93993,
|
||||
"gzipBytes": 28342,
|
||||
"path": "houseplan-assets/de-E17v3gXt.js",
|
||||
"sha256": "ed10d6c5e7a82b275c9422be67c2213219a75dba9aab0ddb3e98813249cbd1d0",
|
||||
"rawBytes": 94196,
|
||||
"gzipBytes": 28393,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-BLL8Ue28.js",
|
||||
"sha256": "6176410d822670169447e2324d7af4ffc7f500e914fd597b0318c41785adb31c",
|
||||
"path": "houseplan-assets/editor-CbeYCXUv.js",
|
||||
"sha256": "75887638c4d76040651a3cf52c56de10dc19e0cbc7583ee169d9b20cc5987788",
|
||||
"rawBytes": 3833,
|
||||
"gzipBytes": 1581,
|
||||
"gzipBytes": 1584,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"sha256": "e45b8023f44f1048304d5d43616f51ea797f9a3475a801f776f9c488b93a1435",
|
||||
"path": "houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"sha256": "cdab9e5e612be9b98c49193a2a6287080c13f62aaefa9a28b69bac94dd934012",
|
||||
"rawBytes": 25600,
|
||||
"gzipBytes": 5056,
|
||||
"isEntry": false,
|
||||
@@ -131,159 +131,159 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-Bho0FfMD.js",
|
||||
"sha256": "ae1939c095197487f36b8d34e06f976ac8e665f3542d9dc9cf06190963310ac3",
|
||||
"rawBytes": 96369,
|
||||
"gzipBytes": 27888,
|
||||
"path": "houseplan-assets/fr-CpDKyK4Z.js",
|
||||
"sha256": "ee9fe5934eea69e549ff48d2e3fdabd7c20e0d9906a6f4623314f6eea48bf269",
|
||||
"rawBytes": 96627,
|
||||
"gzipBytes": 27937,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"sha256": "ae4b691e03b0869b927ab76dfce816e0e50662bb9c78de68d2a66cdd2ead163e",
|
||||
"path": "houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"sha256": "f668d3c0a47a53e096f26728187b1666fbb6b421c1baa2ff1a649fa1ba5340d5",
|
||||
"rawBytes": 52035,
|
||||
"gzipBytes": 16941,
|
||||
"gzipBytes": 16940,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"sha256": "8746be63df29372816cf31f039e60165f1c7906565ab25cef8de52937cd88e89",
|
||||
"rawBytes": 1055297,
|
||||
"gzipBytes": 298290,
|
||||
"path": "houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"sha256": "055b6f8ba3a472be163a15ef6e2348727b5b03e3660b9362f3aed21d7be90426",
|
||||
"rawBytes": 1059790,
|
||||
"gzipBytes": 299520,
|
||||
"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-E17v3gXt.js",
|
||||
"houseplan-assets/editor-CbeYCXUv.js",
|
||||
"houseplan-assets/fr-CpDKyK4Z.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
|
||||
"houseplan-assets/iso-scene-render-BxXtKC5L.js",
|
||||
"houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
|
||||
"houseplan-assets/pdf-export-DL7VmwpB.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-FEM5fqpW.js",
|
||||
"sha256": "1a2c3ca19fd2ecdfe8e4fbbe2b0d187d44dc986cf97a7d5d13a47d0edf2baad0",
|
||||
"rawBytes": 679701,
|
||||
"gzipBytes": 189570,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
|
||||
"sha256": "f25cbcb0453279e3ac64eda67df52af100d96c54bb93cb657bc08c355e0484ed",
|
||||
"rawBytes": 688848,
|
||||
"gzipBytes": 192327,
|
||||
"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-WDIF6iao.js",
|
||||
"houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js",
|
||||
"houseplan-assets/zigbee-topology-DOAnbvlu.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-DZoi-fxk.js",
|
||||
"houseplan-assets/support-fr-Daosy89Q.js",
|
||||
"houseplan-assets/support-ru-e8-BrqtH.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-CiQlpis3.js",
|
||||
"sha256": "08a26f899d0a5f8f71a2ed6f71d095388aa9eead14ce4a4593ce21ee2e54ea14",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
|
||||
"sha256": "4ea3584c8b2bfc86f96c587549ddce49eaf0b918923df2df8e98c9c0a548f835",
|
||||
"rawBytes": 15886,
|
||||
"gzipBytes": 4910,
|
||||
"gzipBytes": 4908,
|
||||
"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-WDIF6iao.js",
|
||||
"houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/hp-zigbee-topology-overlay-BeNZqp-_.js",
|
||||
"sha256": "5360f9165e07d274bc2e71634792f9d8868b20750f4565feeee98d48e9ac08fa",
|
||||
"path": "houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
|
||||
"sha256": "2a0f9af108b8a39889f90d7119d8e7789716c9960ea8e28af38ada07cdc91a8c",
|
||||
"rawBytes": 10958,
|
||||
"gzipBytes": 3764,
|
||||
"gzipBytes": 3765,
|
||||
"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-BYwTiVnu.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js",
|
||||
"houseplan-assets/zigbee-topology-DOAnbvlu.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/iso-scene-render-Soh6e8O4.js",
|
||||
"sha256": "192b514d86778182905c9ade6b2f2f28e8428b281b0190ebe9b159d3170eaf98",
|
||||
"path": "houseplan-assets/iso-scene-render-BxXtKC5L.js",
|
||||
"sha256": "4150eb9120fdaafa745761242ff3285056ceaccd7b016a16364574b90e3e2cfe",
|
||||
"rawBytes": 52970,
|
||||
"gzipBytes": 17885,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/live-interaction-runtime-D6A_AoBE.js",
|
||||
"sha256": "8a3cd740cd67f85041975cf18d9819b6d44be34601e56d639664dce2b79b9873",
|
||||
"rawBytes": 8216,
|
||||
"gzipBytes": 3207,
|
||||
"path": "houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
|
||||
"sha256": "66e70baa00f4c9470ac9a37a229652cf5c5585caa2364de4d467a1c2ebe24358",
|
||||
"rawBytes": 8370,
|
||||
"gzipBytes": 3280,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/namespace-language-CwX4Ljv1.js",
|
||||
"sha256": "46a700cfd83daaaa2ddebe209e8395e08ae6c1e84f286e1f5807c31604e90130",
|
||||
"path": "houseplan-assets/namespace-language-zIuNvgGV.js",
|
||||
"sha256": "244bce2d5211106846215be6a9d3dacda4c910d06020144f9ffd5e432c5e8c00",
|
||||
"rawBytes": 675,
|
||||
"gzipBytes": 409,
|
||||
"gzipBytes": 410,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/pdf-export-De59asII.js",
|
||||
"sha256": "4f1c72412bade0a94e89cc49fb44cce1b90c5304186506d0254fd0de25b26c31",
|
||||
"rawBytes": 236608,
|
||||
"gzipBytes": 109973,
|
||||
"path": "houseplan-assets/pdf-export-DL7VmwpB.js",
|
||||
"sha256": "548dced1a8a54f860dbe57e61019d50f058d3ca13abb738d750a3c107c88de8b",
|
||||
"rawBytes": 236714,
|
||||
"gzipBytes": 109998,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/furniture-plan-art.generated-B7fH8oqt.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-de-BjfMuSw8.js",
|
||||
"sha256": "62cda042ab64811c685f49f5aa338308e592f464f39afc1440de0031cdd97cf0",
|
||||
"path": "houseplan-assets/settings-de-DfJ7EKF9.js",
|
||||
"sha256": "0580b9f6191edb664c21dee21e5263489eaf22876bcad67ef12e440abc03e28a",
|
||||
"rawBytes": 7378,
|
||||
"gzipBytes": 2878,
|
||||
"gzipBytes": 2879,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-fr-BKCX3R6z.js",
|
||||
"sha256": "57246900bc095d6a84a2e7db9c96f7bb634aeb056957c431abbff760bed335db",
|
||||
"path": "houseplan-assets/settings-fr-Bl79E-6s.js",
|
||||
"sha256": "f1bde9db7fe884aabd25130f783a84d8fe27bc9553923c17bcb9b82ee3976475",
|
||||
"rawBytes": 7620,
|
||||
"gzipBytes": 2841,
|
||||
"gzipBytes": 2842,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-ru-D8xvcCw6.js",
|
||||
"sha256": "cf1089c5af8bffe24f153ae8553b8eeb14577862cb34b3ee51e2061b1585091c",
|
||||
"path": "houseplan-assets/settings-ru-CkNFmChi.js",
|
||||
"sha256": "94f96c4a15a3a82ca8be3066629734379dd14eb2ff9e8f6acf369da2378676cb",
|
||||
"rawBytes": 10304,
|
||||
"gzipBytes": 3325,
|
||||
"isEntry": false,
|
||||
@@ -291,47 +291,47 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-editor-CwTXBm1f.js",
|
||||
"sha256": "6ccad563f28ed17d527b6e2a6efc167b696f478c7c23b114bf904ac4e695f566",
|
||||
"path": "houseplan-assets/summary-panel-editor-C_vpFucN.js",
|
||||
"sha256": "d45dbdfa17a1da9f45bfbca6d6c3cc98849fd5ebcc06c8f72fd6310b04b93aad",
|
||||
"rawBytes": 14862,
|
||||
"gzipBytes": 3423,
|
||||
"gzipBytes": 3425,
|
||||
"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-v5DlO12L.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-metrics-Bd81K-sc.js",
|
||||
"sha256": "20851cfa3b7d45452f558ac6ec231d6ba7a0a9b1459760580fa56ce7c5554a05",
|
||||
"path": "houseplan-assets/summary-panel-metrics-D47J-OjO.js",
|
||||
"sha256": "909d2195fcfca30c2f1279e07e2d7506d470c3daec6fe864c97f7ecc4101e33d",
|
||||
"rawBytes": 2701,
|
||||
"gzipBytes": 1344,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-runtime-loaded-jjP-5y3_.js",
|
||||
"sha256": "1ed46af8f90254099376b7107163e5ee6dfb282924409bd5a5fa40036660c2eb",
|
||||
"path": "houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
|
||||
"sha256": "e44860a1af9ec7ffc7ac289c8c76ed1d0dcf0177bd91bbfe1981fdd0d4a62c46",
|
||||
"rawBytes": 72959,
|
||||
"gzipBytes": 20242,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-CCrtJjwP.js",
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/form-kit.styles-v5DlO12L.js",
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/summary-panel-editor-CwTXBm1f.js",
|
||||
"houseplan-assets/summary-panel-metrics-Bd81K-sc.js"
|
||||
"houseplan-assets/summary-panel-editor-C_vpFucN.js",
|
||||
"houseplan-assets/summary-panel-metrics-D47J-OjO.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-de-C9Oo6umO.js",
|
||||
"sha256": "aeab7835cf28821f302612a778f0687b05cb006c5371bbfeed15976ac0aba3c1",
|
||||
"path": "houseplan-assets/support-de-DZoi-fxk.js",
|
||||
"sha256": "f01d2d00a169519f5f402e99e9c85c9f21a23aeee2a9605d4444910f3250d6de",
|
||||
"rawBytes": 6167,
|
||||
"gzipBytes": 2396,
|
||||
"isEntry": false,
|
||||
@@ -339,35 +339,35 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-fr-CRCSYq8q.js",
|
||||
"sha256": "ff04381645035c564a6455ddfbfd81de57960e63977e4979ec1c04b4564f7885",
|
||||
"path": "houseplan-assets/support-fr-Daosy89Q.js",
|
||||
"sha256": "e503a1b4e1b1ed31d8fa09bfe0cdc069a82c1d1627d40407472b336fbc98ccf5",
|
||||
"rawBytes": 6333,
|
||||
"gzipBytes": 2429,
|
||||
"gzipBytes": 2432,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-ru-BcRlKfAd.js",
|
||||
"sha256": "0603d82d78e310ba8eaf26d4cc1b7ceeadf5cfabb40042199cb5b85b1089000c",
|
||||
"path": "houseplan-assets/support-ru-e8-BrqtH.js",
|
||||
"sha256": "76742313c4b510163c380f8fd9c7fcaf1cc7f8bde6a41bcb0da48621a7c3a3b3",
|
||||
"rawBytes": 8376,
|
||||
"gzipBytes": 2875,
|
||||
"gzipBytes": 2876,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-de-BjBB8qiK.js",
|
||||
"sha256": "df225ea5dc6b1886537cd7a0cec055530773ba31c94714a9cc199fd6fae2beb4",
|
||||
"path": "houseplan-assets/topology-de-Cs36ouVE.js",
|
||||
"sha256": "7c808f1139bb1c232627bfe884a579177ec653d16147d6b96066851893b312a4",
|
||||
"rawBytes": 2698,
|
||||
"gzipBytes": 1334,
|
||||
"gzipBytes": 1337,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-fr-DXFxoYd-.js",
|
||||
"sha256": "4c1013cb61db49df76e856c34aea59266e065350210a5d964537f8dd42cf6774",
|
||||
"path": "houseplan-assets/topology-fr-B8yoJGvc.js",
|
||||
"sha256": "6ee1530fd0bd7a8c476c79b83404c7d8a5b98cd0b86ee8a73048be75a70cc2c4",
|
||||
"rawBytes": 2718,
|
||||
"gzipBytes": 1354,
|
||||
"isEntry": false,
|
||||
@@ -375,62 +375,62 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-ru-Di7X71yN.js",
|
||||
"sha256": "daa7bcbbde34f363c8b114b2af050cc19264e85c7bcc6ba31a13e0b963f4f8ce",
|
||||
"path": "houseplan-assets/topology-ru-BRKShmrQ.js",
|
||||
"sha256": "7fc89542fa860318d459ce1467a5a8931f3214a9d17f57c177ba01f2cfbbd3e8",
|
||||
"rawBytes": 3669,
|
||||
"gzipBytes": 1615,
|
||||
"gzipBytes": 1616,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-BbetEPvj.js",
|
||||
"sha256": "a204ece3a16a5af991ed3e6092e8ab2a3cd9d4d2bd79fdc1f056c504cdfa7798",
|
||||
"path": "houseplan-assets/zigbee-topology-DOAnbvlu.js",
|
||||
"sha256": "313f71677ba0739d9c978185c2136e73e8037b770eb3f9ef8561f692d654406f",
|
||||
"rawBytes": 10958,
|
||||
"gzipBytes": 4301,
|
||||
"gzipBytes": 4299,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js",
|
||||
"houseplan-assets/namespace-language-CwX4Ljv1.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/topology-de-BjBB8qiK.js",
|
||||
"houseplan-assets/topology-fr-DXFxoYd-.js",
|
||||
"houseplan-assets/topology-ru-Di7X71yN.js"
|
||||
"houseplan-assets/topology-de-Cs36ouVE.js",
|
||||
"houseplan-assets/topology-fr-B8yoJGvc.js",
|
||||
"houseplan-assets/topology-ru-BRKShmrQ.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-runtime-sjaKOGGU.js",
|
||||
"sha256": "bd75de4445ae8f036fbf89763dead2ce464783b566a6860fd9603e5c9251099b",
|
||||
"path": "houseplan-assets/zigbee-topology-runtime-CKlry6R8.js",
|
||||
"sha256": "39e4a250de0e67e80d8afc02c39c1a79e42d4b3e113107bb3817f4fa90cab1e2",
|
||||
"rawBytes": 3983,
|
||||
"gzipBytes": 1789,
|
||||
"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-BYwTiVnu.js",
|
||||
"houseplan-assets/namespace-language-zIuNvgGV.js",
|
||||
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "efc308c02a842ebafa05186a59d8ad2d734445445f1b9b746e1914706dca87cf",
|
||||
"sha256": "d4b480db1327152db7b1459c45692ea6b72fb7ac3bccb525bc06adfe1aab3b8d",
|
||||
"rawBytes": 1205,
|
||||
"gzipBytes": 803,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-panel.js",
|
||||
"sha256": "f820d606cf77d60252147bdf93075b2621d05e6cd3f014acdeb0c032b6b83d23",
|
||||
"sha256": "5aaa677a9c85f4df0f176be31a91ebb5022f5ce129774919269bb0409c310233",
|
||||
"rawBytes": 6165,
|
||||
"gzipBytes": 2324,
|
||||
"gzipBytes": 2325,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Ddo22-SB.js"
|
||||
"houseplan-assets/houseplan-card-BYwTiVnu.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{aD as e,l as o,aG as t,ez as s,A as a,eA as i,eB as l,eC as n,E as r,c}from"./houseplan-card-BYwTiVnu.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
${d(h.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
|
||||
@@ -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};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{eE as e,eF as n,eB as a}from"./houseplan-card-BYwTiVnu.js";function c(a,c){return new e([{code:"en",dictionary:a},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",console.warn,n)}function o(e){return function(e,n){return{state:a=>{const c=e.state(a);return"pending"!==c&&n.some(e=>"pending"===e.state(a))?"pending":c},dictionary:n=>e.dictionary(n),ensure:a=>Promise.all([e,...n].map(e=>e.ensure(a))).then(()=>{})}}(a,e)}export{c as n,o as s};
|
||||
@@ -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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{c as a,A as e}from"./houseplan-card-BYwTiVnu.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-B0Dq0vzN.js";import"./form-kit.styles-v5DlO12L.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 @@
|
||||
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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{eH as e,N as t,b2 as n,L as i,ae as o,ac as r,c9 as s,eI as a,c8 as c,b1 as l,b0 as d,b3 as u,eJ as f,eK as m,cb as g,eL as v}from"./houseplan-card-BYwTiVnu.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};
|
||||
@@ -1 +1 @@
|
||||
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};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";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="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";export{e as dictionary,n as fingerprint};
|
||||
@@ -1 +1 @@
|
||||
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};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";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="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";export{e as dictionary,s as fingerprint};
|
||||
@@ -1 +1 @@
|
||||
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};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";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="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";export{e as dictionary,t as fingerprint};
|
||||
@@ -1 +1 @@
|
||||
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};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{n as t,c as n}from"./zigbee-topology-DOAnbvlu.js";import{aM as e}from"./houseplan-card-BYwTiVnu.js";import"./namespace-language-zIuNvgGV.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(f(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const f=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";try{await import("./houseplan-assets/houseplan-card-BYwTiVnu.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)}
|
||||
|
||||
@@ -299,6 +299,7 @@ def _project_plan_only_space(space: dict[str, Any]) -> dict[str, Any]:
|
||||
_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 []
|
||||
]
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.78.0-beta.5"
|
||||
"version": "1.78.0"
|
||||
}
|
||||
|
||||
@@ -133,7 +133,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
|
||||
|
||||
|
||||
|
||||
@@ -381,6 +381,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"))
|
||||
|
||||
@@ -1208,7 +1208,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 +1280,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 +1428,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 +1490,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 +1576,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 +1799,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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Synthetic demo home
|
||||
|
||||
A fully fictional house (plans, devices, states) used for README screenshots,
|
||||
the demo GIF and headless smoke tests — so no real home data ever appears in
|
||||
public materials.
|
||||
|
||||
- `srv/demo.html` — self-contained host page: `<ha-icon>`/`<ha-card>` stubs and a
|
||||
fake `hass` (registries, states, `callWS`, `callService`, floors).
|
||||
- `srv/assets/` — generated plan SVGs and `icons.js` (`node demo/gen_icons.mjs`,
|
||||
needs the repo's devDependencies). The card bundle is copied from `dist/`:
|
||||
`npm run bundle:sync` (копия стенда не коммитится, #255).
|
||||
- `serve.mjs` — playwright launcher (route interception, no web server).
|
||||
- `smoke_*.mjs` — feature smoke tests; run with a Chromium installed via
|
||||
`PLAYWRIGHT_BROWSERS_PATH=<dir> npx playwright install chromium-headless-shell`.
|
||||
|
||||
Note for sandboxed sessions: `/tmp` does not survive; this directory is the
|
||||
persistent home of the harness (docs/DEVELOPMENT.md has the LD_LIBRARY_PATH recipe).
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// #291: same-process p95 of the complete config+layout boundary versus the
|
||||
// pre-existing full-candidate clone contract. Batching makes the strict 20%
|
||||
// ratio meaningful even on coarse/loaded CI timers.
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
import { makeLargeHouseFixture } from './fixtures/large-house.mjs';
|
||||
import {
|
||||
canonicalizeConfigGeometry, canonicalizeLayoutGeometry,
|
||||
} from '../test-build/coordinate-canonicalization.js';
|
||||
|
||||
const WARMUPS = 30;
|
||||
const SAMPLES = 120;
|
||||
const BATCH = 10;
|
||||
const MAX_RATIO = 1.2;
|
||||
const fixture = makeLargeHouseFixture();
|
||||
|
||||
const baseline = () => {
|
||||
JSON.parse(JSON.stringify(fixture.config));
|
||||
JSON.parse(JSON.stringify(fixture.layout || {}));
|
||||
};
|
||||
const candidate = () => {
|
||||
canonicalizeConfigGeometry(fixture.config);
|
||||
canonicalizeLayoutGeometry(fixture.layout || {});
|
||||
};
|
||||
const measure = (operation) => {
|
||||
const started = performance.now();
|
||||
for (let index = 0; index < BATCH; index++) operation();
|
||||
return (performance.now() - started) / BATCH;
|
||||
};
|
||||
for (let index = 0; index < WARMUPS; index++) {
|
||||
baseline();
|
||||
candidate();
|
||||
}
|
||||
const baselineSamples = [];
|
||||
const candidateSamples = [];
|
||||
for (let index = 0; index < SAMPLES; index++) {
|
||||
if (index % 2) {
|
||||
candidateSamples.push(measure(candidate));
|
||||
baselineSamples.push(measure(baseline));
|
||||
} else {
|
||||
baselineSamples.push(measure(baseline));
|
||||
candidateSamples.push(measure(candidate));
|
||||
}
|
||||
}
|
||||
const p95 = (values) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * 0.95) - 1];
|
||||
const baselineP95 = p95(baselineSamples);
|
||||
const candidateP95 = p95(candidateSamples);
|
||||
const ratio = candidateP95 / baselineP95;
|
||||
const report = {
|
||||
fixture: 'large-house-v1', samples: SAMPLES, batch: BATCH,
|
||||
baselineP95Ms: baselineP95, candidateP95Ms: candidateP95,
|
||||
ratio, limit: MAX_RATIO, pass: ratio <= MAX_RATIO,
|
||||
};
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
if (!report.pass) process.exitCode = 1;
|
||||
@@ -1,8 +1,9 @@
|
||||
// #600 AC1: парные кадры продукта для сравнения с референсом
|
||||
// docs/design/600-settings-dialogs/ (ACCEPTANCE.md). Диагностическая съёмка, не
|
||||
// golden: кадры не принимаются golden:accept и не участвуют в golden:verify.
|
||||
// Кадры — артефакт проверки, в репозиторий не коммитятся (#681).
|
||||
//
|
||||
// node demo/capture_design_pairs_600.mjs [--out=docs/design/600-settings-dialogs/pairs]
|
||||
// node demo/capture_design_pairs_600.mjs [--out=artifacts/design-pairs-600]
|
||||
//
|
||||
// Четыре диалога × две темы, ширина поверхности 560, скроллер раскрыт на всю
|
||||
// высоту, чтобы кадр показывал форму целиком, как макет референса. Состояния —
|
||||
@@ -14,7 +15,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { launch } from './serve.mjs';
|
||||
|
||||
const out = resolve(process.argv.find((a) => a.startsWith('--out='))?.slice(6) || 'docs/design/600-settings-dialogs/pairs');
|
||||
const out = resolve(process.argv.find((a) => a.startsWith('--out='))?.slice(6) || 'artifacts/design-pairs-600');
|
||||
mkdirSync(out, { recursive: true });
|
||||
|
||||
const THEMES = {
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
/** Explicit #505 diagnostic evidence, not a golden-baseline producer or smoke. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { sourceFingerprint } from '../scripts/source-fingerprint.mjs';
|
||||
import { assertFreshDemoBundle } from './bundle-freshness.mjs';
|
||||
import { HA_DIALOG_PIN, prepareHaDialogAssets } from './helpers/ha-dialog-assets.mjs';
|
||||
import { launchHaDialogFixture } from './helpers/ha-dialog-fixture.mjs';
|
||||
|
||||
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const options = { output: join(repo, 'artifacts/summary-panel-505') };
|
||||
for (let index = 2; index < process.argv.length; index++) {
|
||||
const arg = process.argv[index];
|
||||
if (arg === '--probe-only') options.probeOnly = true;
|
||||
else if (['--output', '--ha-cache', '--ha-wheel', '--only'].includes(arg)) {
|
||||
const value = process.argv[++index];
|
||||
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
||||
if (arg === '--only') options.only = value.split(',');
|
||||
else options[{ '--output': 'output', '--ha-cache': 'cacheDir', '--ha-wheel': 'wheelPath' }[arg]] = resolve(value);
|
||||
} else throw new Error(`Unknown option ${arg}`);
|
||||
}
|
||||
// Fail before the optional 124 MB download. Captures never bypass freshness.
|
||||
const fingerprint = sourceFingerprint(repo);
|
||||
if (!options.probeOnly) {
|
||||
const manifest = JSON.parse(readFileSync(join(repo, 'demo/srv/assets/houseplan-assets.json')));
|
||||
assert.equal(manifest.fingerprint, fingerprint, 'Stale demo bundle: run npm run bundle:sync before capturing');
|
||||
}
|
||||
mkdirSync(options.output, { recursive: true });
|
||||
const report = {
|
||||
issue: 505, diagnosticOnly: true, platform: process.platform,
|
||||
sourceSha: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim(),
|
||||
sourceFingerprint: fingerprint, probeOnly: Boolean(options.probeOnly),
|
||||
selectedCases: options.only || 'full matrix',
|
||||
haFrontend: HA_DIALOG_PIN.version, wheelSha256: HA_DIALOG_PIN.sha256,
|
||||
boundaries: 'Loopback synthetic demo only. No core/auth/HA server. Official app loader exposed at runtime; component factories/CSS unmodified.',
|
||||
comparison: 'Equivalent one block, three default system rows and one entity row. Reference row text is normalized to observed product values; reference CSS/files are unchanged. Floor-plan/fake HA chrome pixels are not parity targets.',
|
||||
captures: [],
|
||||
};
|
||||
const saveReport = () => writeFileSync(join(options.output, 'report.json'), JSON.stringify(report, null, 2));
|
||||
const assets = await prepareHaDialogAssets(options);
|
||||
|
||||
async function probe() {
|
||||
const fixture = await launchHaDialogFixture({ assets });
|
||||
try {
|
||||
const { page } = fixture;
|
||||
report.chromium = fixture.browser.version();
|
||||
await page.goto(`${fixture.url}/ha505-probe.html`);
|
||||
await page.waitForFunction(() => document.querySelector('ha-dialog')?.shadowRoot?.querySelector('wa-dialog')?.shadowRoot?.querySelector('dialog')?.matches(':modal'));
|
||||
await page.waitForTimeout(300);
|
||||
const read = () => page.evaluate(() => {
|
||||
const ha = document.querySelector('ha-dialog');
|
||||
const wa = ha.shadowRoot.querySelector('wa-dialog');
|
||||
const surface = wa.shadowRoot.querySelector('dialog');
|
||||
return { width: surface.getBoundingClientRect().width, modal: surface.matches(':modal'),
|
||||
radius: getComputedStyle(surface).borderRadius, preset: ha.width,
|
||||
inheritedMd: getComputedStyle(ha).getPropertyValue('--ha-dialog-width-md') };
|
||||
});
|
||||
const small = await read();
|
||||
await page.evaluate(async () => { const ha = document.querySelector('ha-dialog'); ha.width = 'medium'; await ha.updateComplete; });
|
||||
await page.waitForTimeout(50);
|
||||
const medium = await read();
|
||||
await page.evaluate(() => document.querySelector('#parent').style.setProperty('--ha-dialog-width-md', '920px'));
|
||||
await page.waitForTimeout(50);
|
||||
const inherited = await read();
|
||||
await page.screenshot({ path: join(options.output, 'authentic-ha-width-920.png') });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForTimeout(100);
|
||||
const mobile = await read();
|
||||
await page.screenshot({ path: join(options.output, 'authentic-ha-mobile-390.png') });
|
||||
assert.equal(small.width, 320); assert.equal(medium.width, 580);
|
||||
assert.equal(inherited.width, 920); assert.equal(mobile.width, 390);
|
||||
await fixture.assertClean();
|
||||
report.probe = { small, medium, inherited, mobile, provenance: fixture.provenance,
|
||||
pageErrors: fixture.errors, externalRequests: fixture.externalRequests, websocketAttempts: fixture.websocketAttempts };
|
||||
saveReport();
|
||||
} finally { await fixture.close(); }
|
||||
}
|
||||
|
||||
const rowLabels = ['Количество устройств', 'Общая площадь комнат', 'Текущие дата и время', 'Температура кухни'];
|
||||
const productConfig = {
|
||||
version: 1, title: 'Сводная информация', show_on_mobile: true,
|
||||
blocks: [{ id: 'block-1', title: 'Общее', visible: true, scope: { type: 'all' },
|
||||
values: rowLabels.map((label, index) => ({ id: `value-${index + 1}`, label,
|
||||
source: index < 3 ? { type: 'system', key: ['device_count', 'total_area', 'datetime'][index] }
|
||||
: { type: 'entity', entity_id: 'sensor.kitchen_temperature' } })) }],
|
||||
};
|
||||
const referenceConfig = { title: productConfig.title, showOnMobile: true, blocks: [{
|
||||
...productConfig.blocks[0], scope: { type: 'all', spaceId: null },
|
||||
values: rowLabels.map((label, index) => ({ id: `value-${index + 1}`, label,
|
||||
source: index < 3 ? { type: 'system', key: ['device_count', 'room_area', 'date_time'][index], name: label }
|
||||
: { type: 'entity', entityId: 'sensor.kitchen_temperature' } })),
|
||||
}] };
|
||||
const panelCases = [
|
||||
{ name: 'desktop-light', width: 1600, height: 1000, theme: 'light' },
|
||||
{ name: 'desktop-dark', width: 1600, height: 1000, theme: 'dark' },
|
||||
{ name: 'bottom-portrait', width: 800, height: 1100, theme: 'light' },
|
||||
{ name: 'kiosk', width: 1600, height: 1000, theme: 'light', kiosk: true },
|
||||
{ name: 'mobile-view', width: 390, height: 844, theme: 'light' },
|
||||
];
|
||||
const settingCases = ['native', 'real-ha'].flatMap((shell) => [
|
||||
{ name: 'desktop', width: 1600, height: 1000 },
|
||||
{ name: 'narrow-320', width: 320, height: 844 },
|
||||
{ name: 'narrow-390', width: 390, height: 844 },
|
||||
].flatMap((size) => ['light', 'dark'].map((theme) => ({ ...size,
|
||||
name: `settings-${shell}-${size.name}-${theme}`, theme, shell, settings: true }))));
|
||||
const edgeCases = ['native', 'real-ha'].flatMap((shell) => [
|
||||
{ name: `edge-${shell}-effective-zoom-200`, width: 800, height: 500,
|
||||
adaptation: '800×500 effective viewport proxy for 1600×1000 at 200%; not actual browser zoom' },
|
||||
{ name: `edge-${shell}-text-200-long-de`, width: 1600, height: 1000, textSize: 32, language: 'de',
|
||||
adaptation: 'Root font-size 32px enlarged-text stress, long German entity name/id; not browser zoom' },
|
||||
{ name: `edge-${shell}-text-200-long-de-390`, width: 390, height: 844, textSize: 32, language: 'de',
|
||||
adaptation: '390px plus root font-size32px and long German source' },
|
||||
].map((scenario) => ({ ...scenario, shell, settings: true, theme: 'light' })));
|
||||
|
||||
function scenarioConfig(scenario, reference = false) {
|
||||
const config = structuredClone(reference ? referenceConfig : productConfig);
|
||||
if (scenario.language === 'de') {
|
||||
config.title = 'Zusammenfassung'; config.blocks[0].title = 'Allgemein';
|
||||
const row = config.blocks[0].values[3];
|
||||
row.label = 'Küchentemperatur und langfristiger Messwert';
|
||||
row.source = { type: 'entity', [reference ? 'entityId' : 'entity_id']:
|
||||
'sensor.kueche_erdgeschoss_langfristige_temperaturmessung_mit_besonders_langer_quellenkennung_505' };
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function configureProduct(page, scenario) {
|
||||
await page.waitForFunction(() => window.__card?._summary && window.__card?._model?.length > 0);
|
||||
await assertFreshDemoBundle(page, repo);
|
||||
await page.waitForFunction(() => window.__card._booting === false && !window.__card._bootFading);
|
||||
await page.evaluate(async ({ scenario, config }) => {
|
||||
const card = window.__card;
|
||||
Object.assign(document.querySelector('#host').style, { width: '100%', height: '100vh', padding: '0', margin: '0', maxWidth: 'none' });
|
||||
Object.assign(document.body.style, { margin: '0', padding: '0' });
|
||||
card.panelHost = true;
|
||||
card.setAttribute('panel-host', '');
|
||||
Object.assign(card.style, { width: '100%', height: '100%' });
|
||||
card.setConfig({ ...card._config, language: scenario.language || 'ru', kiosk: Boolean(scenario.kiosk) });
|
||||
card._serverCfg = { ...card._serverCfg, settings: { ...card._serverCfg.settings, summary_panel: config } };
|
||||
card._cfgEpoch++;
|
||||
card._haSummaryPanelApi = 1;
|
||||
card._serverCanWrite = true;
|
||||
card.narrow = scenario.width <= 450;
|
||||
const dark = scenario.theme === 'dark';
|
||||
const colors = dark ? ['#3ea6ff', '#e6e7eb', '#9aa4ad', '#202126', '#3a3d45', '#11151b', '#2b2d33']
|
||||
: ['#0b73b8', '#202124', '#5f6368', '#ffffff', '#d7d9de', '#eef1f4', '#e7eaee'];
|
||||
['primary-color', 'primary-text-color', 'secondary-text-color', 'card-background-color', 'divider-color', 'primary-background-color', 'secondary-background-color']
|
||||
.forEach((key, index) => document.documentElement.style.setProperty(`--${key}`, colors[index]));
|
||||
document.documentElement.style.setProperty('--ha-card-background', colors[3]);
|
||||
document.documentElement.style.setProperty('--app-header-background-color', colors[3]);
|
||||
document.documentElement.style.setProperty('--app-header-text-color', colors[1]);
|
||||
document.documentElement.style.setProperty('--text-primary-color', '#ffffff');
|
||||
document.documentElement.style.colorScheme = scenario.theme;
|
||||
if (scenario.textSize) document.documentElement.style.fontSize = `${scenario.textSize}px`;
|
||||
document.body.style.background = colors[5];
|
||||
const entityId = config.blocks[0].values[3].source.entity_id;
|
||||
card.hass = { ...card.hass, language: scenario.language || 'ru', locale: { ...card.hass.locale, language: scenario.language || 'ru' },
|
||||
config: { ...card.hass.config, time_zone: 'Europe/Moscow', unit_system: { ...card.hass.config?.unit_system, length: 'm' } },
|
||||
themes: { ...card.hass.themes, darkMode: dark },
|
||||
states: { ...card.hass.states, [entityId]: { entity_id: entityId, state: '20.6', attributes: {
|
||||
friendly_name: scenario.language === 'de' ? 'Küchentemperatur im Erdgeschoss mit langfristiger Messwertüberwachung und besonders langer Quellenbezeichnung' : 'Kitchen temperature', unit_of_measurement: '°C' } } },
|
||||
};
|
||||
card._summary.updated();
|
||||
card._summary.saveLocal({ show: true });
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
card._summary.resized();
|
||||
}, { scenario, config: scenarioConfig(scenario) });
|
||||
await page.waitForFunction(() => window.__card._booting === false && !window.__card._bootFading && window.__card._devices.length > 0);
|
||||
if (!scenario.settings) await page.locator('.summary-overlay').waitFor({ state: 'visible' });
|
||||
await page.waitForTimeout(350);
|
||||
if (scenario.settings) {
|
||||
await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
if (card._ensureEditorRuntime) assertRuntime(await card._ensureEditorRuntime());
|
||||
function assertRuntime(ready) { if (!ready) throw new Error('Editor runtime failed to load'); }
|
||||
await card._summary.openDialog();
|
||||
await card.updateComplete;
|
||||
});
|
||||
await page.locator('hp-dialog[data-kind="summary"] .summary-editor').waitFor({ state: 'visible' });
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
async function capture(scenario) {
|
||||
console.log(`Capturing ${scenario.name}`);
|
||||
const fixture = await launchHaDialogFixture({ assets, authentic: scenario.shell === 'real-ha',
|
||||
viewport: { width: scenario.width, height: scenario.height }, colorScheme: scenario.theme });
|
||||
const record = { ...scenario, files: {} };
|
||||
try {
|
||||
const { page } = fixture;
|
||||
// Let Date.now advance: the real card boot-veil quiescence uses it.
|
||||
await page.clock.setSystemTime(new Date('2026-09-09T09:00:00Z'));
|
||||
await page.goto(`${fixture.url}/product.html`);
|
||||
await configureProduct(page, scenario);
|
||||
const values = await page.evaluate((config) => config.blocks[0].values.map((value) => window.__card._summary.value(value)), scenarioConfig(scenario));
|
||||
assert.equal(values.length, 4, 'Equivalent four-row product fixture');
|
||||
record.product = await page.evaluate((settings) => {
|
||||
const root = window.__card.shadowRoot;
|
||||
const box = (node) => node ? { ...node.getBoundingClientRect().toJSON(), scrollWidth: node.scrollWidth, clientWidth: node.clientWidth } : null;
|
||||
const panel = root.querySelector('.summary-overlay');
|
||||
const hp = root.querySelector('hp-dialog[data-kind="summary"]');
|
||||
const ha = hp?.shadowRoot.querySelector('ha-dialog');
|
||||
const wa = ha?.shadowRoot.querySelector('wa-dialog');
|
||||
const surface = wa?.shadowRoot.querySelector('dialog') || hp?.shadowRoot.querySelector('.surface');
|
||||
const footer = hp?.querySelector('[slot="footer"]');
|
||||
const editor = hp?.querySelector('.summary-editor');
|
||||
const insideViewport = (node) => { const r = node?.getBoundingClientRect(); return !!r && r.left >= -1 && r.right <= innerWidth + 1 && r.top >= -1 && r.bottom <= innerHeight + 1; };
|
||||
return { panel: box(panel), control: box(root.querySelector('.summary-control')), stage: box(root.querySelector('.stage')),
|
||||
side: panel?.classList.contains('bottom') ? 'bottom' : 'right', rows: panel?.querySelectorAll('.summary-value').length,
|
||||
settings: settings ? { surface: box(surface), editor: box(editor), footer: box(footer),
|
||||
actualHA: Boolean(wa), modal: surface?.matches(':modal'), radius: getComputedStyle(surface).borderRadius,
|
||||
border: getComputedStyle(surface).border, shadow: getComputedStyle(surface).boxShadow,
|
||||
rootFont: getComputedStyle(document.documentElement).fontSize,
|
||||
titleFont: getComputedStyle(hp.shadowRoot.querySelector('.title')).fontSize,
|
||||
sourceRows: [...editor.querySelectorAll('.summary-source')].map((node) => ({ box: box(node), text: node.textContent.trim() })),
|
||||
checks: { surfaceInsideViewport: insideViewport(surface), footerVisible: insideViewport(footer),
|
||||
editorNoHorizontalOverflow: editor.scrollWidth <= editor.clientWidth + 1,
|
||||
surfaceNoHorizontalOverflow: surface.scrollWidth <= surface.clientWidth + 1,
|
||||
footerNoHorizontalOverflow: footer.scrollWidth <= footer.clientWidth + 1 },
|
||||
close: box(ha?.shadowRoot.querySelector('ha-icon-button') || hp.shadowRoot.querySelector('.close')) } : null };
|
||||
}, Boolean(scenario.settings));
|
||||
const shot = async (side, area, locator) => {
|
||||
const name = `${scenario.name}-${side}-${area}.png`;
|
||||
if (locator) await locator.screenshot({ path: join(options.output, name) });
|
||||
else await page.screenshot({ path: join(options.output, name) });
|
||||
record.files[`${side}-${area}`] = name;
|
||||
};
|
||||
await shot('product', 'viewport');
|
||||
if (scenario.settings) await shot('product', 'dialog', page.locator(scenario.shell === 'real-ha'
|
||||
? 'hp-dialog[data-kind="summary"] ha-dialog wa-dialog dialog' : 'hp-dialog[data-kind="summary"] .surface'));
|
||||
else {
|
||||
await shot('product', 'panel', page.locator('.summary-overlay'));
|
||||
await shot('product', 'control', page.locator('.summary-control'));
|
||||
}
|
||||
if (scenario.language === 'de') {
|
||||
// The genuine HA owns scrolling in its shadow .body; the native wrapper
|
||||
// owns it in the editor. Follow the composed tree instead of assuming.
|
||||
const measureSource = async (align) => page.locator('.summary-source').last().evaluate((source, align) => {
|
||||
const label = source.querySelector('strong');
|
||||
(align === 'start' ? label : source).scrollIntoView({ block: align, inline: 'nearest', behavior: 'instant' });
|
||||
const hp = source.closest('hp-dialog');
|
||||
const ha = hp.shadowRoot.querySelector('ha-dialog');
|
||||
const header = ha?.shadowRoot.querySelector('ha-dialog-header') || hp.shadowRoot.querySelector('.header');
|
||||
const footer = hp.querySelector('[slot="footer"]');
|
||||
let bandTop = Math.max(0, header?.getBoundingClientRect().bottom || 0);
|
||||
let bandBottom = Math.min(innerHeight, footer.getBoundingClientRect().top);
|
||||
const visible = (node) => { const box = node.getBoundingClientRect().toJSON();
|
||||
const visibleHeight = Math.max(0, Math.min(box.bottom, bandBottom) - Math.max(box.top, bandTop));
|
||||
return { box, visibleHeight, visibleFraction: visibleHeight / box.height }; };
|
||||
const scrollOwners = [];
|
||||
for (let node = source; node; node = node.assignedSlot || node.parentElement || node.getRootNode()?.host) {
|
||||
if (node.scrollHeight > node.clientHeight + 1 && /auto|scroll/.test(getComputedStyle(node).overflowY)) {
|
||||
const clip = node.getBoundingClientRect();
|
||||
bandTop = Math.max(bandTop, clip.top); bandBottom = Math.min(bandBottom, clip.bottom);
|
||||
scrollOwners.push({ tag: node.localName, class: node.className, scrollTop: node.scrollTop,
|
||||
scrollHeight: node.scrollHeight, clientHeight: node.clientHeight });
|
||||
}
|
||||
}
|
||||
return { align, bandTop, bandBottom, label: visible(label), source: visible(source), scrollOwners };
|
||||
}, align);
|
||||
record.product.longSourceStart = await measureSource('start');
|
||||
assert.ok(record.product.longSourceStart.label.visibleHeight > 0, 'Long source label must actually appear between header and footer');
|
||||
await shot('product', 'long-source-scrolled');
|
||||
record.product.longSourceEnd = await measureSource('end');
|
||||
assert.ok(record.product.longSourceEnd.source.visibleHeight > 0, 'Long source end must actually be visible');
|
||||
await shot('product', 'long-source-end');
|
||||
}
|
||||
await fixture.assertClean();
|
||||
await page.addInitScript(({ config, theme, kiosk, textSize }) => {
|
||||
localStorage.setItem('houseplan-dashboard5-config', JSON.stringify(config));
|
||||
localStorage.setItem('houseplan-dashboard5-enabled', 'true');
|
||||
localStorage.setItem('houseplan-dashboard5-theme', theme);
|
||||
localStorage.setItem('houseplan-dashboard5-kiosk', String(Boolean(kiosk)));
|
||||
if (textSize) document.addEventListener('DOMContentLoaded', () => { document.documentElement.style.fontSize = `${textSize}px`; });
|
||||
}, { config: scenarioConfig(scenario, true), theme: scenario.theme, kiosk: scenario.kiosk, textSize: scenario.textSize });
|
||||
await page.goto(`${fixture.url}/reference/index.html`);
|
||||
await page.locator('#dashboard3-panel').waitFor({ state: 'visible' });
|
||||
await page.waitForTimeout(350);
|
||||
await page.locator('.dashboard3-value dd').evaluateAll((nodes, text) => {
|
||||
if (nodes.length !== text.length) throw new Error('Equivalent reference row count mismatch');
|
||||
nodes.forEach((node, index) => { node.textContent = text[index]; });
|
||||
}, values);
|
||||
if (scenario.settings) {
|
||||
await page.locator('.dashboard3-settings-toggle').click();
|
||||
await page.locator('.dashboard3-settings-dialog__window').waitFor({ state: 'visible' });
|
||||
}
|
||||
record.reference = await page.evaluate(() => {
|
||||
const box = (selector) => document.querySelector(selector)?.getBoundingClientRect().toJSON();
|
||||
return { panel: box('#dashboard3-panel'), control: box('.dashboard5-summary-control'),
|
||||
dialog: box('.dashboard3-settings-dialog__window'), normalizedValueText: [...document.querySelectorAll('.dashboard3-value dd')].map((node) => node.textContent) };
|
||||
});
|
||||
await shot('reference', 'viewport');
|
||||
if (scenario.settings) await shot('reference', 'dialog', page.locator('.dashboard3-settings-dialog__window'));
|
||||
else {
|
||||
await shot('reference', 'panel', page.locator('#dashboard3-panel'));
|
||||
await shot('reference', 'control', page.locator('.dashboard5-summary-control'));
|
||||
}
|
||||
await fixture.assertClean();
|
||||
record.pageErrors = fixture.errors; record.externalRequests = fixture.externalRequests;
|
||||
record.websocketAttempts = fixture.websocketAttempts;
|
||||
report.captures.push(record); saveReport();
|
||||
} finally { await fixture.close(); }
|
||||
}
|
||||
|
||||
try {
|
||||
await probe();
|
||||
const cases = [...panelCases, ...settingCases, ...edgeCases];
|
||||
if (options.only) for (const name of options.only) assert.ok(cases.some((scenario) => scenario.name === name), `Unknown case ${name}`);
|
||||
if (!options.probeOnly) for (const scenario of cases.filter((scenario) => !options.only || options.only.includes(scenario.name))) await capture(scenario);
|
||||
report.status = 'complete'; saveReport();
|
||||
console.log(`Diagnostic evidence: ${join(options.output, 'report.json')}`);
|
||||
} catch (error) {
|
||||
report.status = 'failed'; report.error = error.stack || String(error); saveReport(); throw error;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/** Local-only visual proof for #275; external backup contents are never printed. */
|
||||
import { mkdirSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { optimizePlans } from '../test-build/plan-optimizer.js';
|
||||
import { launch } from './serve.mjs';
|
||||
|
||||
const input = process.argv[2];
|
||||
const outputDir = resolve(process.argv[3] || '.');
|
||||
const mode = process.argv.includes('--optimized') ? 'optimized' : 'raw';
|
||||
const nodesArg = process.argv.find((value) => value.startsWith('--nodes='))?.slice(8) || '';
|
||||
const nodes = nodesArg.split(';').filter(Boolean).map((pair) => pair.split(',').map(Number));
|
||||
if (!input) {
|
||||
console.error('usage: node demo/capture_wall_strip_backup.mjs <backup> <outdir> '
|
||||
+ '[--optimized] [--nodes=x,y;x,y]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const backup = JSON.parse(readFileSync(input, 'utf8'));
|
||||
const payload = backup?.payload && typeof backup.payload === 'object' ? backup.payload : backup;
|
||||
const source = {
|
||||
config: payload?.config && typeof payload.config === 'object' ? payload.config : payload,
|
||||
layout: payload?.layout && typeof payload.layout === 'object' ? payload.layout : {},
|
||||
};
|
||||
const rendered = mode === 'optimized'
|
||||
? optimizePlans(source.config, source.layout)
|
||||
: source;
|
||||
const config = rendered.config;
|
||||
const layout = rendered.layout;
|
||||
if (!Array.isArray(config?.spaces) || !config.spaces.length) {
|
||||
throw new Error('backup has no spaces');
|
||||
}
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const { page, browser } = await launch({ width: 1800, height: 1250 }, 1);
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.evaluate(async ({ cfg, lay }) => {
|
||||
const card = window.__card;
|
||||
card._serverCfg = structuredClone(cfg);
|
||||
card._layout = structuredClone(lay);
|
||||
card._space = cfg.spaces[0].id;
|
||||
card._setMode('plan');
|
||||
card._tool = 'select';
|
||||
card._cfgEpoch++;
|
||||
card._modelCache = null;
|
||||
card._frame = null;
|
||||
card._wallUnionCache = null;
|
||||
card._physicalBodiesCache = null;
|
||||
card._lightBarrierCache = null;
|
||||
card._isoGeometryCache.clear();
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
card._fitAll();
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
|
||||
}, { cfg: config, lay: layout });
|
||||
|
||||
const fullPath = resolve(outputDir, `${mode}-full.png`);
|
||||
await page.screenshot({ path: fullPath, animations: 'disabled' });
|
||||
for (let index = 0; index < nodes.length; index++) {
|
||||
const center = await page.evaluate(([x, y]) => {
|
||||
const card = window.__card;
|
||||
const svg = (card.shadowRoot || card.renderRoot).querySelector('.stage svg');
|
||||
const matrix = svg?.getScreenCTM?.();
|
||||
if (!matrix) return null;
|
||||
const point = new DOMPoint(x * 1000, y * 1000).matrixTransform(matrix);
|
||||
return { x: point.x, y: point.y };
|
||||
}, nodes[index]);
|
||||
if (!center) continue;
|
||||
const width = 420, height = 360;
|
||||
const clip = {
|
||||
x: Math.max(0, Math.min(1800 - width, center.x - width / 2)),
|
||||
y: Math.max(0, Math.min(1250 - height, center.y - height / 2)),
|
||||
width,
|
||||
height,
|
||||
};
|
||||
await page.screenshot({
|
||||
path: resolve(outputDir, `${mode}-node-${index + 1}.png`),
|
||||
clip,
|
||||
animations: 'disabled',
|
||||
});
|
||||
}
|
||||
await browser.close();
|
||||
console.log(JSON.stringify({ mode, full: fullPath, crops: nodes.length }));
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"schema": 3,
|
||||
"matrixVersion": 66,
|
||||
"acceptedAt": "2026-09-27T07:07:07.508Z",
|
||||
"sourceFingerprint": "9f31bdc1556b108a5f8910aa9f246ba40507335bdeb00c8b1b2d960f72f84ce7",
|
||||
"matrixVersion": 67,
|
||||
"acceptedAt": "2026-09-28T13:36:48.059Z",
|
||||
"sourceFingerprint": "baa0806921c7f72959047c7d2bda343ef5e378c4e8d2a8e612f74b6af2e2c5b1",
|
||||
"chromium": "151.0.7922.34",
|
||||
"capturedOn": "linux",
|
||||
"acceptedOn": "linux",
|
||||
@@ -10,41 +10,18 @@
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"chromium": "151.0.7922.34",
|
||||
"buildFingerprint": "9f31bdc1556b108a5f8910aa9f246ba40507335bdeb00c8b1b2d960f72f84ce7",
|
||||
"ci": null
|
||||
},
|
||||
"localAttestation": {
|
||||
"schema": "houseplan-golden-wsl/v1",
|
||||
"sha256": "adb377c40236510ed767a5a829dbbc88e51156e8d1f6eb206166fd3a7c61f921",
|
||||
"artifactSha256": "bbcaf4d2621c381224d34fa24d7c14bebb357d2de6342d4ba278a8b9f48556dd",
|
||||
"createdAt": "2026-09-27T07:05:53.848Z",
|
||||
"source": {
|
||||
"buildFingerprint": "baa0806921c7f72959047c7d2bda343ef5e378c4e8d2a8e612f74b6af2e2c5b1",
|
||||
"ci": {
|
||||
"repository": "Matysh/houseplan-card",
|
||||
"branch": "issue/673-stage6-golden",
|
||||
"commit": "c9a5df602becb3f402cf906c8d9b6b30898439fd",
|
||||
"tree": "d7f59614ce1d307175670c39687e4988b41aaaf8",
|
||||
"remoteSha": "c9a5df602becb3f402cf906c8d9b6b30898439fd",
|
||||
"clean": true,
|
||||
"status": ""
|
||||
},
|
||||
"environment": {
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"kernel": "6.18.33.2-microsoft-standard-WSL2",
|
||||
"wsl": true,
|
||||
"distro": "Ubuntu",
|
||||
"filesystem": "ext2/ext3"
|
||||
},
|
||||
"toolchain": {
|
||||
"node": "22.23.2",
|
||||
"npm": "10.9.8",
|
||||
"playwright": "1.62.0",
|
||||
"chromiumExecutableSha256": "0b20b130e7edd9dd51873be867761295fe0cfad490c2b9a64f95bd3cfc08fa71"
|
||||
"run": 36427848424,
|
||||
"attempt": 1,
|
||||
"sha": "c1289886175e165c5be09e4bd700a572529da234"
|
||||
}
|
||||
},
|
||||
"localAttestation": null,
|
||||
"foreignCapture": null,
|
||||
"witnesses": {
|
||||
"count": 126,
|
||||
"count": 107,
|
||||
"floor": 10
|
||||
},
|
||||
"scenarios": {
|
||||
@@ -53,10 +30,10 @@
|
||||
"isometric-stage6-dark-darkfloor": "5b76746846c13c2dbca9ade31d4e6cb891a9a486029ebcf4008539a4a92b47fc",
|
||||
"isometric-stage6-dark-lightfloor": "157e0aaf68396b8ed5acc0d2caed935dc0d17c014c265cb2aaeda66944bba8c2",
|
||||
"isometric-stage6-hover-light": "3f8dc5052e883a30e3cf89c1a0fdea0af3bae3c763b6ebaa50e41cba9cdb3bfd",
|
||||
"stairs-flat-normal-light": "df6600545ffbc7611b764eb489e6e3f625ccf51b6ed2f8f83e4df5b7e1565d08",
|
||||
"stairs-flat-hover-dark": "e642373ea6680a978219622105552d4afbf2311eebf101a7c8db280373c9eca3",
|
||||
"stairs-flat-selected-light": "4b37e663d5689c3288ad7dc956b2ec0b42e406d418873c0ea6089c1caf92ec83",
|
||||
"stairs-isometric-dark": "901bc1eee214d179aff13bc7f192a9e270688aa59b2744f3b361e653dc2d0af5",
|
||||
"stairs-flat-normal-light": "abd698576bb726bc70226deb72c12c3b7ebf36dd3007186e5d9444d630075bef",
|
||||
"stairs-flat-hover-dark": "2502c9526ce73017e465bd35a8609afa0dbcac8dfa34ef37cfad8b3f39ebcd52",
|
||||
"stairs-flat-selected-light": "c395ae3969521f6a157ff6e9460971db26287f9a5ca73f6ecbba6e8d35c3424f",
|
||||
"stairs-isometric-dark": "038bb2752cb98b652541fdc6307f89e85ddd46212014e1991f7086cb42911ab3",
|
||||
"panel-wide-view-light-en": "275d09b3730361bb491a832fbb76723d90a7327086f042cbf0fdd79f4a002800",
|
||||
"panel-wide-plan-editor-dark-en": "093a3fd1022755f5128261796e009383d7e7a23e9e50f193ab1987d30f9313b2",
|
||||
"panel-narrow-read-only-empty-light-en": "99b8f1af82b1ad13f237125aab8f7e7da0563c138e9da2ddf06e54c2d56138c4",
|
||||
@@ -92,9 +69,9 @@
|
||||
"pdf-export-stepped-dimensions-light": "b9732b8b9c38153c564d0c93fbfbf44a8dd348c8667d2f8c2d14492e08e04f3e",
|
||||
"vacuum-trail-smoothing-dark": "f8eecc726a3e8bdaba414584ad6afd48908ee9d96ea969a1e37758f4e76843e2",
|
||||
"room-label-parity-view-dark": "4c00ee3119d3886a5906d5880769558083eb06547a2e2c56e91ae84498416bc9",
|
||||
"room-label-parity-plan-dark": "c0f08f586116c4070723ec89697aa5d966ea57f1c027e7740eeb5147d44d9958",
|
||||
"room-label-parity-plan-dark": "88374fbca15362da485ff32a0c5dce21bd07ee752087a863e10484537b18a930",
|
||||
"room-label-parity-view-light": "ec2a1294b0f08019f49a392cdc94970c1545673ebe0d7f22652f602adf00d4e8",
|
||||
"room-label-parity-plan-light": "2603d0c73b39fc72f8b31919fcee15d8b17ea3c6ba1dea74fc791efdd3cfafc6",
|
||||
"room-label-parity-plan-light": "1ee88b46a711e9116fefbeb8205618d68ddaa99fa3f290f0881e696afd1d1e57",
|
||||
"washer-active-cycle-dark": "0d683619c27747b3dd6a1e6c7cdc5e15dd886f4f2db4b85b7ca5a5c529e9ec52",
|
||||
"washer-idle-cycle-dark": "0138902daa8dabb0588904507ef5a44f5257f7859b4f1198191bfca3dd4130b7",
|
||||
"day-cycle-dawn-dark": "96bd5ef84345976bb83d35d418e7d247503629eceb732b6e926f2217f3b9faa4",
|
||||
@@ -124,6 +101,10 @@
|
||||
"opening-placement-passage-thick-wall-dark": "8d089364f6f9aace8a0cccad952731e6d295440b9eb1c8cb75b6e2264b8fa5c5",
|
||||
"opening-placement-passage-thick-wall-light": "14c06cd119a476f8a4edb63db37d55c6d4000f6fbb3ed412cb88e225574896a6",
|
||||
"opening-symbol-room-wall-light": "dc3f6513a08b809a77df6822f59fdb30c912f320e55d47d749963678fbb8f417",
|
||||
"static-hatch-openings-132-dpr1-light": "4d9f858e7d88113a3d65ed5ce96d7da3a60d0a9bb4a520d4522c59c6b1cadd1c",
|
||||
"static-hatch-openings-140-dpr1-dark": "6e1571b0e49771d2e657dddc4fdd53ad64f0234ca0bd127c34a72f23e076319a",
|
||||
"static-hatch-openings-132-dpr2-dark": "ea27355d19b6a4142a67dd37dab8498315047c01d517251b23059521659f6165",
|
||||
"static-hatch-openings-140-dpr2-light": "482795eb2186c1cf4b914568a0c3349a864fce6b69c3c05d40a780e75f61e148",
|
||||
"opening-symbol-diagonal-partition-dark": "fe264aa0a5926d904ea0a2d15e5f0567a536c9251a72bf96a287d7bf9c480aa1",
|
||||
"opening-symbol-flip-pairs-light": "693fe97bae8375c6a16244fbb4ddaa1ed447732382a45d4629e1711e0aa0d45a",
|
||||
"isometric-opening-symbol-parity-dark": "5dfcd219d9cdc41c382d6e436f15665c0a265ba2b7138b2856e1a50f0e252062",
|
||||
@@ -183,7 +164,7 @@
|
||||
"junction-x-90-equal15-dark": "f01ecc2b28d457fbff0581681dc8c1a1555558baf4c0ac8e2bd4bdd3637f51b4",
|
||||
"junction-x-90-mixed-dark": "1218a50ad968eae651e61637eeaed8238d0598d77749d3c9cdb7bdd7c0d6a063",
|
||||
"junction-x-45-alternating-dark": "08de6b9d3a0e6d0164efc014fb1845cdd70cccb95b05b0d55175f2bca89c378b",
|
||||
"junction-star5-equal15-dark": "4bc9337b4bd2ac4bcb8d168166330faad20184c8b3e51b153ef91d0ad76124c1",
|
||||
"junction-star5-equal15-dark": "08a2410949b60cad38885004c01821935fd04dd1aed15880593069fab6db7e06",
|
||||
"junction-y-60-equal50-dark": "89994b12fd168b3e35ee47520fafa502f904391f98cec909fccac19b7b0a90e5",
|
||||
"junction-acute30-mixed-dark": "bc4f61f377711df71950ea6a8bea7d185c94b5e03bfc2e8fc4930fa9adc9ca02",
|
||||
"junction-acute15-equal50-dark": "3883df35644df7cc169cacb96a5ee8391d64878930c69a0d511cd0cca0f2a04e",
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -1393,6 +1393,31 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
}
|
||||
if (scenario.staticHatchSharpness) {
|
||||
const root = card.renderRoot;
|
||||
const scene = root.querySelector('.plan-svg');
|
||||
const view = card._viewOr(card._baseVb());
|
||||
const exactViewBox = `${view.x} ${view.y} ${view.w} ${view.h}`;
|
||||
const scenes = [...root.querySelectorAll('[data-hp-live-viewbox]')];
|
||||
const layers = [...root.querySelectorAll('[data-hp-live-layer="camera"]')];
|
||||
const pattern = root.querySelector('defs > pattern#hp-wall-hatch');
|
||||
const visibleKinds = new Set([...root.querySelectorAll('.opening[data-kind]')]
|
||||
.map((node) => node.getAttribute('data-kind')));
|
||||
const modelKinds = new Set((card._openingsR || []).map((opening) => opening.type));
|
||||
const settled = scene?.getAttribute('viewBox') === exactViewBox
|
||||
&& scenes.every((node) => ['', 'none'].includes(getComputedStyle(node).transform))
|
||||
&& scenes.every((node) => !getComputedStyle(node).willChange
|
||||
|| getComputedStyle(node).willChange === 'auto')
|
||||
&& layers.every((node) => ['', 'none'].includes(getComputedStyle(node).transform));
|
||||
// #689: the historical pattern at every scale (#685's gradient reverted).
|
||||
const hatch = !!pattern && !root.querySelector('linearGradient#hp-wall-hatch');
|
||||
const openings = ['door', 'window', 'gate'].every((kind) => visibleKinds.has(kind))
|
||||
&& modelKinds.has('passage');
|
||||
if (!settled || !hatch || !openings
|
||||
|| Math.abs(card._zoom - scenario.zoom) > 1e-9) {
|
||||
throw new Error(`static hatch sharpness contract failed: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
if (scenario.wallJunctionPreview) {
|
||||
const { path, pointer, cms, cm } = scenario.wallJunctionPreview;
|
||||
const validPoint = (point) => Array.isArray(point) && point.length === 2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
|
||||
|
||||
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
|
||||
export const GOLDEN_MATRIX_VERSION = 66;
|
||||
export const GOLDEN_MATRIX_VERSION = 67;
|
||||
|
||||
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
|
||||
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
|
||||
@@ -107,9 +107,13 @@ const stage3VacuumTrail = {
|
||||
|
||||
const stage3RequiredOverlays = ['device', 'room-label', 'opening-lock'];
|
||||
|
||||
/** Exact existing baseline impact measured against origin/dev with the same
|
||||
* Chromium build. Keep this list reviewable: shared visual fixtures make an
|
||||
* opening-only change visible in editor/dialog captures too. */
|
||||
/** Exact existing baseline impact of the opening-symbol change (#242/#250, matrix
|
||||
* v37): measured by comparing `actualSha256` for HEAD and origin/dev under the
|
||||
* same Chromium build (baseline status alone was not used — dev already had
|
||||
* unrelated pending candidates). Every listed frame uses a shared fixture with
|
||||
* an affected opening or keeps that plan behind an editor/dialog; no other
|
||||
* existing frame changed. Keep this list reviewable: this is its only copy
|
||||
* since the testing-notes checklists were removed (#681). */
|
||||
export const OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT = Object.freeze([
|
||||
'isometric-geometry-view-dark',
|
||||
'isometric-geometry-view-light',
|
||||
@@ -209,6 +213,8 @@ const stairLayerFixture = [
|
||||
target_space_id: 'golden-lighting' },
|
||||
{ id: 'golden-stair-straight-small', kind: 'straight', x: 0.72, y: 0.27,
|
||||
length: 0.14, width: 0.065, angle: 45, direction: 'backward',
|
||||
// #683 AC13: explicit user-owned style stays identical in light/dark themes.
|
||||
color: '#0066cc', opacity: 0.85, fill_color: '#ffd23f', fill_opacity: 0.55,
|
||||
target_space_id: 'golden-lighting' },
|
||||
{ id: 'golden-stair-spiral-large', kind: 'spiral', x: 0.25, y: 0.73,
|
||||
radius: 0.10, angle: 15, direction: 'clockwise',
|
||||
@@ -647,6 +653,21 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
{ id: 'opening-symbol-room-wall-light', fixture: 'visual', space: 'golden-opening-symbols',
|
||||
mode: 'view', openingSymbolContract: openingRoomContract,
|
||||
theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
...[
|
||||
['static-hatch-openings-132-dpr1-light', 1.32, 1, 'light'],
|
||||
['static-hatch-openings-140-dpr1-dark', 1.40, 1, 'dark'],
|
||||
['static-hatch-openings-132-dpr2-dark', 1.32, 2, 'dark'],
|
||||
['static-hatch-openings-140-dpr2-light', 1.40, 2, 'light'],
|
||||
].map(([id, zoom, deviceScaleFactor, theme]) => ({
|
||||
id, fixture: 'visual', space: 'golden-opening-symbols', mode: 'view',
|
||||
openingSymbolContract: openingRoomContract,
|
||||
extraOpenings: [{
|
||||
id: 'sharpness-passage', type: 'passage', x: 0.62, y: 0.50,
|
||||
angle: 0, length: 0.06,
|
||||
}],
|
||||
staticHatchSharpness: true, zoom, zoomCenter: [500, 500],
|
||||
deviceScaleFactor, theme, viewport: { width: 1000, height: 900 }, ...stage,
|
||||
})),
|
||||
{ id: 'opening-symbol-diagonal-partition-dark', fixture: 'visual', space: 'golden-opening-symbols',
|
||||
mode: 'view', openingSymbolContract: openingPartitionContract,
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
одно исключение внутри страницы и обязан выйти с кодом 1.
|
||||
|
||||
Новый benchmark, открывающий страницу Playwright, обязан получить такой же
|
||||
режим и запись здесь: этого требует `test/smoke-harness-contract.test.mjs`, и
|
||||
режим и запись в `verify-guard.mjs` (`file: '../<имя>.mjs', args:
|
||||
['--guard-probe']`): этого требует `test/smoke-harness-contract.test.mjs`, и
|
||||
требует статически — потому что динамическое доказательство возможно только
|
||||
там, где есть браузер.
|
||||
там, где есть браузер. Этот README записи не заменяет.
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# Offline authentic HA dialog evidence (#505)
|
||||
|
||||
This is an explicit diagnostic, **not** a `smoke_*` entry, a golden-baseline
|
||||
producer, or a Home Assistant server. It never connects to a real HA instance.
|
||||
Issue/spec: [#505](https://github.com/Matysh/houseplan-card/issues/505),
|
||||
[`505-summary-panel-design-parity.md`](../../docs/specs/505-summary-panel-design-parity.md).
|
||||
|
||||
The same pinned fixture also verifies #607 against the real HA close button:
|
||||
|
||||
```sh
|
||||
npm run bundle:sync
|
||||
node demo/verify_ha_dialog_discard_recovery.mjs
|
||||
```
|
||||
|
||||
That diagnostic opens Device, Room, Space and General settings in turn. For
|
||||
each form it edits a field, presses HA's own close button, chooses Continue,
|
||||
proves that the same modal and draft are interactive again, then covers
|
||||
Discard and a clean close. It also fails on duplicate `hp-close` events,
|
||||
external requests, WebSockets or browser errors. The ordinary native-fallback
|
||||
contract remains in `demo/smoke_dialog_modal_recovery.mjs`.
|
||||
|
||||
Issue #609 uses that same unmodified pinned `ha-dialog` to verify the shared
|
||||
settings-form shell rather than the close lifecycle:
|
||||
|
||||
```sh
|
||||
npm run bundle:sync
|
||||
node demo/verify_ha_form_shell_609.mjs --capture
|
||||
```
|
||||
|
||||
It opens the real Room settings form and checks the settled WebAwesome surface:
|
||||
560 px desktop width, the 940 px/viewport height cap, zero duplicate HA body
|
||||
padding, one HA-owned scroller, a distinct form canvas, footer containment and
|
||||
edge-to-edge layouts at 480×800 and 390×844. It also keeps HA's own fullscreen
|
||||
behaviour for a 1280×480 viewport. `--capture` writes the three reviewed diagnostic
|
||||
images under `docs/design/600-settings-dialogs/pairs/`; without it the command
|
||||
is read-only. The authentic diagnostic explicitly applies the light palette,
|
||||
then repeats desktop geometry in the dark palette with a 32 px root font and
|
||||
proves that canvas contrast, the single HA scroller and the footer survive.
|
||||
The fast Validate witness is
|
||||
`demo/smoke_ha_form_shell_parity.mjs`; neither diagnostic is a golden producer.
|
||||
|
||||
From a checkout with the usual `npm ci` dependencies and Playwright Chromium:
|
||||
|
||||
```sh
|
||||
node demo/capture_summary_panel_505.mjs --probe-only
|
||||
node demo/capture_summary_panel_505.mjs --output artifacts/summary-panel-505
|
||||
```
|
||||
|
||||
The second command requires an already fresh `npm run bundle:sync` result.
|
||||
It never builds or changes product/reference files, and refuses stale source
|
||||
fingerprints even when `HP_ALLOW_STALE_BUNDLE` is set. For reviewer work, use
|
||||
`--output "$RUNNER_TEMP/summary-panel-505"` to keep all evidence outside the checkout.
|
||||
Use `--only edge-real-ha-text-200-long-de-390` for a named focused recapture;
|
||||
comma-separated names are accepted and recorded as a partial selection.
|
||||
|
||||
The first explicit run downloads the official 124,294,469-byte
|
||||
`home-assistant-frontend==20260729.7` wheel from the exact allowlisted PyPI file
|
||||
URL; version, size and SHA-256 are pinned in `ha-dialog-assets.mjs`, checked
|
||||
before extraction, and checked against `tests_backend/requirements.txt`.
|
||||
The wheel is **not installed**. Only allowlisted modern JS/static assets are
|
||||
extracted to the OS temporary cache. The deterministic cache manifest has its
|
||||
own pinned hash; existing extracted bytes are then checked against its per-file
|
||||
hashes each run, so edited cache metadata is not trusted. Use a new cache directory
|
||||
after corruption; the tool does not delete existing data.
|
||||
|
||||
To reuse an already downloaded official wheel or select a disposable cache:
|
||||
|
||||
```sh
|
||||
node demo/capture_summary_panel_505.mjs --probe-only --ha-wheel /path/to/home_assistant_frontend-20260729.7-py3-none-any.whl --ha-cache /path/to/cache --output /path/to/evidence
|
||||
```
|
||||
|
||||
`HP_HA_DIALOG_WHEEL` also supplies a local wheel. All paths are caller-selected;
|
||||
there are no author-machine paths in the helper. Cache contents and screenshots
|
||||
are not committed.
|
||||
|
||||
## Authenticity and safety boundary
|
||||
|
||||
The official `app.d53ce8172fc8c85d.js` is served with one explicit runtime
|
||||
instrumentation: its final `o(91535);` becomes
|
||||
`window.__ha505Require=o;o(91535);`, exposing its otherwise private module
|
||||
loader. The downloaded wheel and the `ha-dialog`, WebAwesome, Lit and other
|
||||
component factories/styles remain unmodified. Numbered official ESM chunks
|
||||
register their original factories with that loader; there is no dialog stub.
|
||||
This is a genuine-component diagnostic, **not** a complete production HA boot.
|
||||
|
||||
No `core.*` entry, authentication root or HA server is loaded. The server binds
|
||||
only `127.0.0.1` on an ephemeral port. A restrictive CSP, browser request
|
||||
allowlist, blocked service workers and throwing WebSocket constructor prohibit
|
||||
external/HA requests; any attempted external request, WebSocket or page error
|
||||
fails the run. The synthetic demo HTML is adapted only in its served response
|
||||
to await the authentic component bootstrap and avoid duplicate icon/card tag
|
||||
registration. No production HA configuration, devices or services are touched.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
`report.json` records source SHA/fingerprint, Chromium/platform, wheel pin/hash,
|
||||
measured surface/editor/footer/control geometry, actual shadow-component
|
||||
presence, typography and network errors. The standalone probe proves the
|
||||
small320/medium580/public inherited-width920 contract plus mobile390 fullscreen.
|
||||
It does not load House Plan; the full settings cases exercise actual `hp-dialog`.
|
||||
|
||||
The full command writes paired viewport/region PNGs for five panel contexts
|
||||
(desktop light/dark, portrait bottom, kiosk, mobile View), native/real-HA
|
||||
settings at desktop/320/390 in both themes, and enlarged-text/long-German
|
||||
source edge cases. One default block has three system rows plus one entity row
|
||||
on both sides. Reference displayed values are normalized to measured product
|
||||
values **only as fixture text**, not CSS; original reference files stay intact.
|
||||
The archived prototype UI is Russian; German stress changes fixture data and
|
||||
product locale, not the archived prototype UI translation.
|
||||
Long-source start/end images use `scrollIntoView` and record the actual composed
|
||||
scroll owner plus source/label visibility between header and footer. This matters
|
||||
because genuine HA scrolls its shadow body, not necessarily `.summary-editor`.
|
||||
|
||||
The 800×500 case is explicitly an effective-viewport proxy for a 1600×1000
|
||||
display at 200%, **not actual browser zoom**. Separate 32px root-font cases
|
||||
exercise enlarged text. HA normally uses a 14px root, so the report includes
|
||||
actual computed font sizes. Different fake HA chrome/floor-plan pixels are not
|
||||
parity targets. Fullscreen HA rules, HA typography/theme, larger touch targets
|
||||
and the native disabled Save action remain the spec's allowed adaptations.
|
||||
|
||||
`status: complete` means capture completed, not design acceptance. Inspect the
|
||||
PNGs and the settings `checks`; these files are diagnostic evidence outside
|
||||
canonical `demo/golden/baselines` and docs screenshot manifests. Regenerate on
|
||||
the reviewer's pinned checkout and record the exact command/report paths.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Offline authentic HA dialog fixture (#505, #607, #609)
|
||||
|
||||
`demo/helpers/ha-dialog-fixture.mjs` serves the synthetic demo together with the
|
||||
official, pinned Home Assistant `ha-dialog` so a diagnostic can exercise the
|
||||
real component. It is **not** a `smoke_*` entry, a golden-baseline producer, or
|
||||
a Home Assistant server, and it never connects to a real HA instance. It was
|
||||
built for the #505 summary-panel parity work
|
||||
([`505-summary-panel-design-parity.md`](../../docs/specs/505-summary-panel-design-parity.md));
|
||||
that one-off capture script and its designer prototype were removed by #681 and
|
||||
remain in git history. Two diagnostics use the fixture today.
|
||||
|
||||
#607 verifies the real HA close button:
|
||||
|
||||
```sh
|
||||
npm run bundle:sync
|
||||
node demo/verify_ha_dialog_discard_recovery.mjs
|
||||
```
|
||||
|
||||
That diagnostic opens Device, Room, Space and General settings in turn. For
|
||||
each form it edits a field, presses HA's own close button, chooses Continue,
|
||||
proves that the same modal and draft are interactive again, then covers
|
||||
Discard and a clean close. It also fails on duplicate `hp-close` events,
|
||||
external requests, WebSockets or browser errors. The ordinary native-fallback
|
||||
contract remains in `demo/smoke_dialog_modal_recovery.mjs`.
|
||||
|
||||
Issue #609 uses that same unmodified pinned `ha-dialog` to verify the shared
|
||||
settings-form shell rather than the close lifecycle:
|
||||
|
||||
```sh
|
||||
npm run bundle:sync
|
||||
node demo/verify_ha_form_shell_609.mjs --capture
|
||||
```
|
||||
|
||||
It opens the real Room settings form and checks the settled WebAwesome surface:
|
||||
560 px desktop width, the 940 px/viewport height cap, zero duplicate HA body
|
||||
padding, one HA-owned scroller, a distinct form canvas, footer containment and
|
||||
edge-to-edge layouts at 480×800 and 390×844. It also keeps HA's own fullscreen
|
||||
behaviour for a 1280×480 viewport. `--capture` writes the diagnostic images into
|
||||
the ignored `artifacts/ha-form-shell-609/`; without it the command is
|
||||
read-only. The authentic diagnostic explicitly applies the light palette,
|
||||
then repeats desktop geometry in the dark palette with a 32 px root font and
|
||||
proves that canvas contrast, the single HA scroller and the footer survive.
|
||||
The fast Validate witness is
|
||||
`demo/smoke_ha_form_shell_parity.mjs`; neither diagnostic is a golden producer.
|
||||
|
||||
The first explicit run downloads the official 124,294,469-byte
|
||||
`home-assistant-frontend==20260729.7` wheel from the exact allowlisted PyPI file
|
||||
URL; version, size and SHA-256 are pinned in `ha-dialog-assets.mjs`, checked
|
||||
before extraction, and checked against `tests_backend/requirements.txt`.
|
||||
The wheel is **not installed**. Only allowlisted modern JS/static assets are
|
||||
extracted to the OS temporary cache. The deterministic cache manifest has its
|
||||
own pinned hash; existing extracted bytes are then checked against its per-file
|
||||
hashes each run, so edited cache metadata is not trusted. After corruption,
|
||||
remove that temporary cache directory; the tool does not delete existing data.
|
||||
|
||||
To reuse an already downloaded official wheel, point `HP_HA_DIALOG_WHEEL` at
|
||||
it. There are no author-machine paths in the helper; cache contents and
|
||||
screenshots are never committed.
|
||||
|
||||
## Authenticity and safety boundary
|
||||
|
||||
The official `app.d53ce8172fc8c85d.js` is served with one explicit runtime
|
||||
instrumentation: its final `o(91535);` becomes
|
||||
`window.__ha505Require=o;o(91535);`, exposing its otherwise private module
|
||||
loader. The downloaded wheel and the `ha-dialog`, WebAwesome, Lit and other
|
||||
component factories/styles remain unmodified. Numbered official ESM chunks
|
||||
register their original factories with that loader; there is no dialog stub.
|
||||
This is a genuine-component diagnostic, **not** a complete production HA boot.
|
||||
|
||||
No `core.*` entry, authentication root or HA server is loaded. The server binds
|
||||
only `127.0.0.1` on an ephemeral port. A restrictive CSP, browser request
|
||||
allowlist, blocked service workers and throwing WebSocket constructor prohibit
|
||||
external/HA requests; any attempted external request, WebSocket or page error
|
||||
fails the run. The synthetic demo HTML is adapted only in its served response
|
||||
to await the authentic component bootstrap and avoid duplicate icon/card tag
|
||||
registration. No production HA configuration, devices or services are touched.
|
||||
@@ -70,10 +70,7 @@ export async function launchHaDialogFixture({
|
||||
} else {
|
||||
let base = join(REPO, 'demo/srv'), relative = pathname;
|
||||
if (pathname === '/product.html') relative = '/demo.html';
|
||||
if (pathname.startsWith('/reference/')) {
|
||||
base = join(REPO, 'docs/design/505-summary-panel/reference');
|
||||
relative = pathname.slice('/reference'.length);
|
||||
} else if (ha && (pathname.startsWith('/frontend_latest/') || pathname.startsWith('/static/'))) base = ha.root;
|
||||
if (ha && (pathname.startsWith('/frontend_latest/') || pathname.startsWith('/static/'))) base = ha.root;
|
||||
const file = resolve(base, '.' + relative);
|
||||
if (!file.startsWith(resolve(base) + sep) || !existsSync(file) || !statSync(file).isFile()) {
|
||||
response.writeHead(404).end('Not found'); return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Backdrop image (docs/BACKDROP.md) — two shots for the owner:
|
||||
// Backdrop image (docs/DECOR-EDITOR.md §3) — two shots for the owner:
|
||||
// backdrop_frame — the transform frame mid-gesture: dashed outline, four
|
||||
// finger-sized corner handles, and the live "W × H" badge
|
||||
// stating the picture's real size through cell_cm;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Capture: a curtain on the move — the semantic transition ring around a
|
||||
// NEUTRAL plate (owner 2026-08-03). The pulse is frozen at a visible frame so
|
||||
// the shot is deterministic.
|
||||
import { launch } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 820, height: 760 }, 2);
|
||||
await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
c._serverCfg = { ...c._serverCfg, markers: [
|
||||
{ id: 'm_gate', binding: 'device:d_gate', tap_action: 'cover', display: 'icon_ripple' },
|
||||
{ id: 'm_mower', binding: 'device:d_mower', hidden: true },
|
||||
] };
|
||||
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
'cover.gate': { entity_id: 'cover.gate', state: 'opening',
|
||||
attributes: { friendly_name: 'Curtain', device_class: 'curtain' } } } };
|
||||
c._setMode('view'); c._space = 'garden';
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
const st = document.createElement('style');
|
||||
st.textContent = '.device-pulse.continuous.transition i:first-child{animation-delay:-1.1s!important;animation-play-state:paused!important;}';
|
||||
(c.shadowRoot || c.renderRoot).appendChild(st);
|
||||
});
|
||||
await page.waitForTimeout(400);
|
||||
const box = await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const r = (c.shadowRoot || c.renderRoot).querySelector('.dev.activity-transition').getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
});
|
||||
const pad = 70;
|
||||
await page.screenshot({ path: process.argv[2] || '/tmp/cover_move.png',
|
||||
clip: { x: Math.max(0, box.x - pad), y: Math.max(0, box.y - pad), width: box.w + pad * 2, height: box.h + pad * 2 } });
|
||||
await browser.close();
|
||||
console.log('shot ok');
|
||||
@@ -1,70 +0,0 @@
|
||||
// Capture: one curtain in the four states an owner sees — closed, open,
|
||||
// opening, closing (owner's contract 2026-08-04). The plate is the plain
|
||||
// neutral badge in ALL of them; open/closed is told by the icon morph alone,
|
||||
// and the two travelling ones add the breathing transition ring (frozen at a
|
||||
// visible frame so the shot is deterministic).
|
||||
import { launch } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 900, height: 520 }, 2);
|
||||
const STATES = ['closed', 'open', 'opening', 'closing'];
|
||||
await page.evaluate(async (STATES) => {
|
||||
const c = window.__card;
|
||||
const devices = {}; const entities = {}; const states = {};
|
||||
STATES.forEach((s, i) => {
|
||||
const id = 'd_cur' + i;
|
||||
devices[id] = { id, name: 'Curtain ' + s, model: 'Roller shade driver E1',
|
||||
area_id: 'garden', identifiers: [['demo', id]], entry_type: null, via_device_id: null };
|
||||
entities['cover.cur' + i] = { entity_id: 'cover.cur' + i, device_id: id, platform: 'demo' };
|
||||
states['cover.cur' + i] = { entity_id: 'cover.cur' + i, state: s,
|
||||
attributes: { friendly_name: 'Curtain ' + s, device_class: 'curtain' } };
|
||||
});
|
||||
c.hass = { ...c.hass,
|
||||
devices: { ...c.hass.devices, ...devices },
|
||||
entities: { ...c.hass.entities, ...entities },
|
||||
states: { ...c.hass.states, ...states } };
|
||||
c._serverCfg = { ...c._serverCfg, markers: [
|
||||
...STATES.map((s, i) => ({ id: 'm_cur' + i, binding: 'device:d_cur' + i,
|
||||
tap_action: 'cover', display: 'icon_ripple' })),
|
||||
{ id: 'm_mower', binding: 'device:d_mower', hidden: true },
|
||||
{ id: 'm_gate', binding: 'device:d_gate', hidden: true },
|
||||
] };
|
||||
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
|
||||
const layout = { ...c._layout };
|
||||
STATES.forEach((s, i) => {
|
||||
const d = c._devices.find((x) => x.bindingRef === 'd_cur' + i);
|
||||
if (d) layout[d.id] = { s: 'garden', x: 0.18 + i * 0.215, y: 0.5 };
|
||||
});
|
||||
c._layout = layout;
|
||||
c._setMode('view'); c._space = 'garden';
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
const st = document.createElement('style');
|
||||
st.textContent = '.device-pulse.continuous.transition i:first-child{animation-delay:-1.1s!important;animation-play-state:paused!important;}';
|
||||
(c.shadowRoot || c.renderRoot).appendChild(st);
|
||||
}, STATES);
|
||||
await page.waitForTimeout(500);
|
||||
// captions under each badge, so the four states are readable in the file
|
||||
const box = await page.evaluate((STATES) => {
|
||||
const c = window.__card;
|
||||
const els = [...(c.shadowRoot || c.renderRoot).querySelectorAll('.devlayer .dev')]
|
||||
.sort((a, b) => a.getBoundingClientRect().x - b.getBoundingClientRect().x);
|
||||
let minX = 1e9, maxX = -1e9, minY = 1e9, maxY = -1e9;
|
||||
els.forEach((el, i) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
const cap = document.createElement('div');
|
||||
cap.textContent = STATES[i];
|
||||
cap.style.cssText = `position:fixed;left:${r.x + r.width / 2}px;top:${r.y + r.height + 10}px;`
|
||||
+ 'transform:translateX(-50%);font:600 13px system-ui,sans-serif;color:#1c2530;'
|
||||
+ 'letter-spacing:.02em;z-index:9999;padding:2px 7px;border-radius:6px;'
|
||||
+ 'background:rgba(255,255,255,0.88);white-space:nowrap;';
|
||||
document.body.appendChild(cap);
|
||||
minX = Math.min(minX, r.x); maxX = Math.max(maxX, r.x + r.width);
|
||||
minY = Math.min(minY, r.y); maxY = Math.max(maxY, r.y + r.height);
|
||||
});
|
||||
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
||||
}, STATES);
|
||||
const pad = 55;
|
||||
await page.screenshot({ path: process.argv[2] || '/tmp/cover_states.png',
|
||||
clip: { x: Math.max(0, box.x - pad), y: Math.max(0, box.y - pad),
|
||||
width: box.w + pad * 2, height: box.h + pad * 2 + 22 } });
|
||||
await browser.close();
|
||||
console.log('shot ok');
|
||||
@@ -1,37 +0,0 @@
|
||||
// Four stills of the #146 day-cycle environment.
|
||||
import { launch } from './serve.mjs';
|
||||
const outDir = process.argv[2] || '/tmp';
|
||||
const { page, browser } = await launch({ width: 700, height: 700 }, 1);
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sp = c._serverCfg.spaces.find((s) => s.id === 'f1');
|
||||
sp.openings = [
|
||||
{ id: 'wN', type: 'window', x: 0.30, y: 0.14, angle: 0, length: 0.08 },
|
||||
{ id: 'wE', type: 'window', x: 0.96, y: 0.60, angle: 90, length: 0.08 },
|
||||
{ id: 'wW', type: 'window', x: 0.04, y: 0.30, angle: 90, length: 0.08 },
|
||||
];
|
||||
sp.plan_url = ''; // hand-drawn plan: white paper — the risky case for a white day
|
||||
sp.settings = { ...(sp.settings || {}), show_borders: true, fill_mode: 'glow' };
|
||||
c._serverCfg.settings = { ...(c._serverCfg.settings || {}), north_deg: 0, sun_rays: true, bg_mode: 'daynight' };
|
||||
c._cfgRev = (c._cfgRev || 0) + 1;
|
||||
});
|
||||
const shot = async (az, el, rising, name) => {
|
||||
await page.evaluate(async ([az, el, rising]) => {
|
||||
const c = window.__card;
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states, 'sun.sun': {
|
||||
entity_id: 'sun.sun', state: el > 0 ? 'above_horizon' : 'below_horizon',
|
||||
attributes: { azimuth: az, elevation: el, rising },
|
||||
} } };
|
||||
c.requestUpdate(); await c.updateComplete;
|
||||
}, [az, el, rising]);
|
||||
await page.waitForTimeout(300);
|
||||
const stage = await page.evaluateHandle(() => window.__card.shadowRoot.querySelector('.stage'));
|
||||
await stage.asElement().screenshot({ path: `${outDir}/${name}.png` });
|
||||
};
|
||||
await shot(95, -2, true, 'dn_dawn');
|
||||
await shot(180, 60, false, 'dn_noon');
|
||||
await shot(265, -2, false, 'dn_sunset');
|
||||
await shot(0, -20, false, 'dn_night');
|
||||
await browser.close();
|
||||
console.log('shots done');
|
||||
@@ -1,63 +0,0 @@
|
||||
// Скриншоты библиотеки мебели: палитра и расставленная мебель на плане.
|
||||
import { launch } from './serve.mjs';
|
||||
const OUT = process.env.HP_SHOT_DIR || '/tmp';
|
||||
const { page, browser } = await launch({ width: 900, height: 900 }, 2);
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
sr.querySelectorAll('.modetab')[2].click();
|
||||
await c.updateComplete;
|
||||
c._curSpaceCfg.decor = [];
|
||||
c._decorTool = 'furniture';
|
||||
c._cfgEpoch++; c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
sr.querySelector('.furnitem[data-symbol="sofa"]').click();
|
||||
await c.updateComplete;
|
||||
});
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: `${OUT}/furniture_palette.png` });
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const P = 1000 / 240;
|
||||
const cm = (v) => (v / 5) * P / 1000; // cm -> normalised
|
||||
const put = (id, symbol, w, h, cx, cy, angle) => ({
|
||||
id, kind: 'furniture', symbol,
|
||||
x: cx / 1000 - cm(w) / 2, y: cy / 1000 - cm(h) / 2,
|
||||
w: cm(w), h: cm(h), color: '#8d6e63', width: 3,
|
||||
...(angle ? { angle } : {}),
|
||||
});
|
||||
// r1 living 40..550 x 140..580 | r2 kitchen 550..960 x 140..460
|
||||
// r3 bedroom 550..960 x 460..860 | r4 hall 40..550 x 580..860
|
||||
c._curSpaceCfg.decor = [
|
||||
put('f1', 'sofa', 220, 90, 250, 140 + 37.5),
|
||||
put('f2', 'coffee_table', 110, 60, 250, 300),
|
||||
put('f3', 'tv', 120, 30, 250, 570),
|
||||
put('f4', 'armchair', 90, 85, 90, 340, 90),
|
||||
put('f5', 'table_dining', 140, 80, 700, 300),
|
||||
put('f6', 'chair', 45, 45, 700, 240),
|
||||
put('f7', 'chair', 45, 45, 700, 360, 180),
|
||||
put('f8', 'fridge', 60, 65, 590, 140 + 27, 0),
|
||||
put('f9', 'stove', 60, 60, 670, 140 + 25),
|
||||
put('f10', 'kitchen_sink', 80, 60, 760, 140 + 25),
|
||||
put('f11', 'dishwasher', 60, 60, 840, 140 + 25),
|
||||
put('f12', 'bed_double', 160, 200, 700, 460 + 42),
|
||||
put('f13', 'nightstand', 45, 40, 620, 470),
|
||||
put('f14', 'nightstand', 45, 40, 780, 470),
|
||||
put('f15', 'wardrobe', 100, 60, 910, 700, 90),
|
||||
put('f16', 'washer', 60, 60, 120, 860 - 25, 180),
|
||||
put('f17', 'toilet', 40, 70, 220, 860 - 29, 180),
|
||||
put('f18', 'bathtub', 170, 75, 400, 860 - 31, 180),
|
||||
put('f19', 'plant', 40, 40, 500, 620),
|
||||
put('f20', 'shower', 90, 90, 490, 700),
|
||||
];
|
||||
c._decorTool = 'select';
|
||||
c._decorSel = 'f1';
|
||||
c._cfgEpoch++; c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/furniture_plan.png` });
|
||||
await browser.close();
|
||||
console.log('shots written to', OUT);
|
||||
@@ -1,34 +0,0 @@
|
||||
import { launch } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 820, height: 760 }, 2);
|
||||
await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
c._serverCfg.markers = (c._serverCfg.markers || []).filter((m) => m.binding !== 'device:d_motion');
|
||||
c._serverCfg.markers.push({ id: 'd_motion', binding: 'device:d_motion', display: 'icon_ripple' });
|
||||
c._cfgEpoch++; c._regSignature = ''; c._maybeRebuildDevices();
|
||||
// the flash is keyed to a WITNESSED off→on transition (owner's rule
|
||||
// 2026-08-01: разовая вспышка в момент обнаружения) — cycle through off
|
||||
const set = async (state) => {
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
'binary_sensor.hall_motion': { entity_id: 'binary_sensor.hall_motion', state,
|
||||
attributes: { friendly_name: 'Motion', device_class: 'motion', linkquality: 64 } } } };
|
||||
await c.updateComplete;
|
||||
};
|
||||
await set('off');
|
||||
await set('on');
|
||||
// freeze the pulse at a visible frame for a deterministic capture
|
||||
const st = document.createElement('style');
|
||||
st.textContent = '.device-pulse.short.event i{animation-delay:-0.4s!important;animation-play-state:paused!important;}';
|
||||
(c.shadowRoot || c.renderRoot).appendChild(st);
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
const box = await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const el = (c.shadowRoot || c.renderRoot).querySelector('.dev.activity-event');
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
});
|
||||
const pad = 70;
|
||||
await page.screenshot({ path: process.argv[2] || '/tmp/motion_sense.png',
|
||||
clip: { x: Math.max(0, box.x - pad), y: Math.max(0, box.y - pad), width: box.w + pad * 2, height: box.h + pad * 2 } });
|
||||
await browser.close();
|
||||
console.log('shot ok');
|
||||
@@ -1,27 +0,0 @@
|
||||
// Capture: the shoulder rulers + centre tick while PLACING a new opening
|
||||
// (Opening tool, cursor hovering the wall centre) — owner 2026-08-03.
|
||||
import { launch } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 900, height: 820 }, 2);
|
||||
await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const sp = c._serverCfg.spaces.find((s) => s.id === 'f1');
|
||||
sp.openings = [];
|
||||
c._setMode('plan'); c._activateOpeningPlacement('door');
|
||||
c._cfgEpoch++; c.requestUpdate();
|
||||
return c.updateComplete && true;
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
const pt = await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const stage = c.renderRoot.querySelector('.stage');
|
||||
const r = stage.getBoundingClientRect();
|
||||
const [vx, vy, vw, vh] = stage.querySelector('svg').getAttribute('viewBox').split(' ').map(Number);
|
||||
return { x: r.left + ((293.7 - vx) / vw) * r.width, y: r.top + ((141 - vy) / vh) * r.height,
|
||||
sx: r.left, sy: r.top, sw: r.width, sh: r.height };
|
||||
});
|
||||
await page.mouse.move(pt.x, pt.y, { steps: 4 });
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: process.argv[2] || '/tmp/opening_place.png',
|
||||
clip: { x: pt.sx, y: pt.sy, width: pt.sw, height: Math.min(pt.sh, 420) } });
|
||||
await browser.close();
|
||||
console.log('shot ok');
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Backdrop image: move, uniform scale, and the new paper rule (docs/BACKDROP.md).
|
||||
* Backdrop image: move, uniform scale, and the new paper rule (docs/DECOR-EDITOR.md §3).
|
||||
*
|
||||
* The whole contract, on the demo's f1 — which IS an image plan, so every
|
||||
* assertion here lands on exactly the case that used to behave differently:
|
||||
@@ -234,7 +234,7 @@ check('reset_button_gone_again', await q('.editor-secondary .bdreset'), 0);
|
||||
|
||||
// ---------- 5b) the SELECT tool still pans right over the picture ---------
|
||||
// The body of the picture is most of the screen; claiming it outside its own
|
||||
// tool would take away the one-finger pan (docs/BACKDROP.md §2), which is why
|
||||
// tool would take away the one-finger pan (docs/DECOR-EDITOR.md §3.2), which is why
|
||||
// moving is a tool. smoke_pan_any_zoom guards the same thing from the outside.
|
||||
await tool('select');
|
||||
await settle();
|
||||
|
||||
@@ -211,7 +211,7 @@ checkAll(res);
|
||||
// .hp-paper shapes sit under everything the plan draws. Since v1.58.0 the
|
||||
// paper is the ROOM CONTOURS and ONLY them — one shape per room, never their
|
||||
// bounding box (section 12), and never the backdrop image rect either
|
||||
// (docs/BACKDROP.md §3). The demo's f1 IS an image plan, so this section now
|
||||
// (docs/DECOR-EDITOR.md §3.3). The demo's f1 IS an image plan, so this section now
|
||||
// asserts the new rule on exactly the case that used to be the exception.
|
||||
// The scene colour is visible ONLY around the paper. The four-phase
|
||||
// environment changes only outside it and adds an alpha-aware outer outline.
|
||||
|
||||
@@ -151,8 +151,8 @@ const out = await page.evaluate(async () => {
|
||||
// the cover, which is what this section pins.) Same cause, same helper:
|
||||
// _stateClass and
|
||||
// the icon morph read d.primary, so the plan reported the state of
|
||||
// `switch.*_reverse_direction`. The rule (docs/FILTERING.md «What a marker
|
||||
// SHOWS»): the marker indicates the entity its tap ACTS ON — the cover
|
||||
// `switch.*_reverse_direction`. The rule (docs/DEVICE-PRESENTATION.md «Source precedence: what a marker
|
||||
// shows»): the marker indicates the entity its tap ACTS ON — the cover
|
||||
// exactly when the owner has explicitly chosen «Открыть/закрыть».
|
||||
const devEl = (dev) => {
|
||||
// The viewport may refit asynchronously after an editor transition. Use
|
||||
|
||||
@@ -289,7 +289,10 @@ checks.terminalFrameKeepsBudget = settled.layers.every(
|
||||
checks.settledPlanLayerIsExplicitAndBounded = !!settledPlanLayer
|
||||
&& settledPlanLayer.width <= active.stage.width * 1.5 + 64
|
||||
&& settledPlanLayer.height <= active.stage.height * 1.5 + 64
|
||||
&& settledPlanLayer.reasons.some((reason) => reason.includes('will-change: transform'))
|
||||
// #689: explicit, but through the opacity hint — the transform hint
|
||||
// freezes the raster scale and the plan stays blurry after a zoom.
|
||||
&& settledPlanLayer.reasons.some((reason) => reason.includes('will-change: opacity'))
|
||||
&& settledPlanLayer.reasons.every((reason) => !reason.includes('will-change: transform'))
|
||||
&& settledPlanLayer.reasons.every((reason) => !reason.includes('Overlaps other composited content'));
|
||||
checks.capturedPresentedPinchFrames = frameMetrics.length >= 3;
|
||||
checks.presentedFramesHaveNoWhiteTile = frameMetrics.every(
|
||||
|
||||