Compare commits

...
85 Commits
Author SHA1 Message Date
Matysh 7d4d75bdfb release: promote v1.78.0 from beta line
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Waiting to run
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #616
Issue: #649
Issue: #651
Issue: #654
Issue: #660
Issue: #663
Issue: #676
Issue: #683
Issue: #688
Issue: #691
User-Visible: yes
Release: v1.78.0
2026-09-28 21:07:36 +03:00
Matysh 9e5f211b0a chore(release): include main CI history before v1.78.0 promotion 2026-09-28 21:05:10 +03:00
Matysh 1ae0ea1c3b Release v1.78.0-beta.9 candidate
Issue: #688
Issue: #689
Issue: #691
User-Visible: yes
Release: v1.78.0-beta.9
2026-09-28 20:44:43 +03:00
claude[bot] db2224c964 docs(reviews): индекс после сдвига каталога (#689)
Issue: #689
User-Visible: no
2026-09-28 17:26:33 +00:00
claude[bot] 4b23679d3c docs: review document for #689
Issue: #689
User-Visible: no
2026-09-28 17:26:30 +00:00
Claude f2b66ad677 docs(screenshots): accept the #689 source fingerprint, pixels unchanged
The docs-screenshots workflow run 36430098138 (issue/689-daycycle-layers
before #691) decoded to the committed pixels in all 11 frames; its bytes
differ only by the oxipng repack. After the rebase onto #691,
`npm run docs:accept -- --identical` confirmed 11/11 pixel-identical
frames on the combined tree, so only the source fingerprint moves
(4955edd1 -> 62c5c18b); frames, their sha and the capture packer stay.

Release: v1.78.0-beta.9
Issue: #689
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:54:32 +03:00
Claude 836ef7710f chore(golden): accept the #689 frames from Linux CI
29 reviewed frames from the golden-images artifact of Validate run
36427848424 (c1289886), 159 kept byte-for-byte:

- 21 junction-* and large-house-zoom-040/250 frames return to their
  pre-#685 look: each equals its baseline from before 139dceaa (no pixel
  off by more than 8 levels). #685's gradient had also doubled the visible
  stripe width (25 % of the step instead of the historical 12.5 %).
- 4 static-hatch-openings-* close-ups (#685's scenes, kept) now show the
  restored pattern.
- 4 stairs-* frames carry #688's single 3.6 cm stair line weight: #688 was
  merged into dev without accepting them; named here explicitly (#690).

No day/night frame moved.

Release: v1.78.0-beta.9
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36427848424
Issue: #689
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
Claude 87195c2b5d test(daycycle): #582/#544 witnesses follow the bounded, hint-free layers
smoke_live_pan_coverage now expects an exposed scene to open only within
clip-path: inset(-25%) and the clip-marked day-cycle outline never to open;
idle scenes keep no clip-path. paper-scene-contract pins the opacity hint.

The Release trailer asks the branch's push Validate for the heavy jobs
(smokes, golden, performance): #689 moves golden frames and must be proven
by the full smoke set before S7 (#690).

Release: v1.78.0-beta.9
Issue: #689
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
Claude 600330e187 fix(daycycle): stage-sized plan layers without a frozen raster; revert #685 hatch
With the day/night background the plan went blurry after zooming from 100 %
(sharp when the page was opened at 800 %) and navigating a strongly zoomed
plan flashed the page white. Both came from #582's composition, not from the
wall hatch that #685 replaced:

- `.stage.daycycle.hp-safe-daycycle-outline .plan-svg` promoted the scene
  with `will-change: transform`; Chromium freezes the raster scale of such a
  layer, so the 100 % raster was shown stretched. The explicit layer #582
  needs is now `will-change: opacity` (re-rasters at the current scale).
- The filtered outline had `overflow: visible` and a gesture exposed every
  scene (#544) without bound, so the promoted layers grew with zoom squared
  (CDP LayerTree, ~460 %: plan-svg 15.9x, outline 13.2x the stage; 39x after
  navigating at 800 %). The full card clips its outline to its box and marks
  it data-hp-live-overflow="clip" (never exposed); the live viewport bounds
  every other exposure with an inline clip-path: inset(-25%) that leaves
  with it, so idle DOM stays byte-identical (#531).

Owner-verified in Chrome 152 (built-in browser, DPR 2): sharp after 100 ->
800 %, no white flashes after reloading at 800 %.

Owner decision: #685's analytic gradient is reverted (13af1d5e), the single
<pattern> is back at every zoom; its close-up golden scenes stay and check
the pattern, its terminal-frame smoke checks the pattern.

Witnesses: demo/smoke_daycycle_zoom_layers.mjs (800 % x DPR 2: layers vs
stage, the hint, reload path, button/wheel/pinch); #582/#532 smokes now pin
the opacity hint; test/live-viewport.test.mjs (bounded exposure, clipped
scene); test/daycycle-layers.test.mjs (cascade). Four Node-guarded mutants.

Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
claude[bot] c1e05e465b docs(reviews): индекс после сдвига каталога (#691)
Issue: #691
User-Visible: no
2026-09-28 14:32:55 +00:00
claude[bot] 0a09712bc5 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 14:32:51 +00:00
Matysh 33ae14a99d test(touch): align kiosk smoke and Russian guide (#691)
Issue: #691
User-Visible: no
2026-09-28 17:17:08 +03:00
claude[bot] 09f8cf4c11 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 14:14:26 +00:00
Matysh 53503e0373 chore(metrics): accept touch controller bundle growth (#691)
Issue: #691
User-Visible: no
2026-09-28 16:33:33 +03:00
Matysh 37fbb9824d fix(touch): separate pan, edge swipe and double fit (#691)
Issue: #691
User-Visible: yes
2026-09-28 16:27:45 +03:00
claude[bot] 3f17a1497d docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 12:56:39 +00:00
claude[bot] 20f17fee54 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 12:51:01 +00:00
claude[bot] 1751184b12 docs: review document for #689
Issue: #689
User-Visible: no
2026-09-28 12:30:23 +00:00
claude[bot] 63c24784b6 docs(reviews): индекс после сдвига каталога (#688)
Issue: #688
User-Visible: no
2026-09-28 12:21:14 +00:00
claude[bot] 8207a175d6 docs: review document for #688
Issue: #688
User-Visible: no
2026-09-28 12:21:12 +00:00
Matysh c04d0778ae fix(stairs): use one physical line weight (#688)
Issue: #688
User-Visible: yes
2026-09-28 14:58:33 +03:00
claude[bot] e44fbd6c9e docs: review document for #688
Issue: #688
User-Visible: no
2026-09-28 11:48:48 +00:00
Matysh c047ca2230 Release v1.78.0-beta.8 candidate
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #684
Issue: #685
Issue: #686
Issue: #687
User-Visible: yes
Release: v1.78.0-beta.8
2026-09-28 13:19:11 +03:00
claude[bot] cd59b2eb66 docs(reviews): индекс после сдвига каталога (#685)
Issue: #685
User-Visible: no
2026-09-28 10:07:31 +00:00
claude[bot] 52b1b2e7c9 docs: review document for #685
Issue: #685
User-Visible: no
2026-09-28 10:07:29 +00:00
Matysh 52fe4d6499 fix: вернуть paint-server штриховки в defs (#685)
Полный smoke обнаружил нарушение порядка SVG-слоёв и устаревшее ожидание скрытых устройств после #687. Paint-server снова живёт в defs; общий mode-smoke проверяет видимые ориентиры устройств в Plan.

Issue: #685
User-Visible: no
2026-09-28 12:04:48 +03:00
Matysh 005b7c406d chore(golden): принять ориентиры устройств из dev (#685)
После автоматического ребейза #687 намеренно показывает неинтерактивные устройства-ориентиры в Plan. Из Linux CI приняты ровно две затронутые сцены; ещё 186 кадров совпали.

Release: v1.78.0-beta.8
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36397286486
Issue: #685
User-Visible: no
2026-09-28 11:35:03 +03:00
Matysh 0a5b34a571 chore: освежить производные гейты после ребейза (#685)
Все 11 кадров документации остались пиксельно идентичными; обновлён только fingerprint актуального дерева. Потолок initial View перецентрирован на 100 Б при факте 299713 Б, без изменения общего бюджета.

Release: v1.78.0-beta.8
Issue: #685
User-Visible: no
2026-09-28 11:20:25 +03:00
Matyshandclaude[bot] c37b5b92bc chore(golden): обновить аттестацию после ребейза (#685)
Все 188 golden-сцен и 11 кадров документации совпали пиксель-в-пиксель после переноса на актуальный dev; обновлены только паспорта текущего дерева.

Issue: #685
User-Visible: no
Release: v1.78.0-beta.8
Baseline-Reviewed-Local: sha256:c02067a968b83ee2ec2f110ab06fb3707491a79a98e61448f4b1ee8cc34c1a9a
2026-09-28 07:35:39 +00:00
Matyshandclaude[bot] 139dceaa4f chore(golden): принять резкую штриховку после зума (#685)
WSL-приёмка обновляет 21 существующую масштабную сцену и добавляет четыре проверки 132/140%, DPR 1/2 с проёмами. Восемь кадров документации пересняты в той же доказанной среде; три неизменных кадра подтвердили совместимость.

Issue: #685
User-Visible: no
Release: v1.78.0-beta.8
Baseline-Reviewed-Local: sha256:414e55783bc8272d7b2ff209a94bd69e12de95f6759d5bc95975acb94e937f39
2026-09-28 07:35:39 +00:00
Sergey Matyuninandclaude[bot] 13af1d5e30 fix: стабилизировать штриховку после зума (#685)
Issue: #685
User-Visible: yes
2026-09-28 07:35:39 +00:00
claude[bot] e8e7ecc9f2 docs: review document for #685
Issue: #685
User-Visible: no
2026-09-28 07:35:39 +00:00
claude[bot] 1493145ba5 docs(reviews): индекс после сдвига каталога (#687)
Issue: #687
User-Visible: no
2026-09-28 07:26:29 +00:00
claude[bot] b1bbf4b076 docs: review document for #687
Issue: #687
User-Visible: no
2026-09-28 07:26:25 +00:00
Claude 128af2392a test(plan): Node witness for the landmark cascade; smoke via __hpTest
The three #687 mutants now name a Node suite over the compiled Plan
stylesheet (test/plan-device-landmarks.test.mjs), as the #683 stair
cursor mutant does: the reviewed browser-guard inventory is at its cap
(200/200, #659). The suite pins no hiding in .stage.markup, the
filter: opacity(0.35) fade without an opacity override, the pointer
boundary on the marker, its subtree and ::before (and Background's),
and a fade scoped to the Plan stage. Parity of the 35% with Background
is a computed-style fact and stays with the smoke (#624 forbids text
reads of the monolith). The smoke now enters modes and tools through
window.__hpTest instead of private writes (#629).

Issue: #687
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 09:59:52 +03:00
Claude 560849a1a7 feat(plan): device markers are landmarks in the Plan editor
The Plan editor hid every device marker (display: none), so walls,
openings and stairs were placed against devices from memory. It now
shows them exactly as the Background editor does (#362): the same
markers as View, at the Background editor's effective opacity, fully
pointer-inert. The fade is filter: opacity(0.35) on each marker, not
on .devlayer, because room labels and room settings buttons share that
layer and stay opaque and interactive; filter multiplies the marker's
own opacity (.unavail -> 0.35 x 0.35), as the Background layer does.
Marker handlers already fail closed outside View/Devices.

demo/smoke_plan_device_landmarks.mjs proves AC1/AC2: same markers as
View, per-marker alpha equal to Background, core and capsule fall
through, a real click on a marker reaches the Walls tool and a room
settings button below, cursor and target unchanged, handlers inert, no
tabindex. Three mutants guard hiding, opacity override and the pointer
boundary. UX-MODES, DECOR-EDITOR, USER-GUIDE.ru mode table, changelogs.

Issue: #687
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 09:38:26 +03:00
claude[bot] cab4b4153a docs: review document for #687
Issue: #687
User-Visible: no
2026-09-28 06:36:32 +00:00
claude[bot] a0b471ca1b docs(reviews): индекс после сдвига каталога (#684)
Issue: #684
User-Visible: no
2026-09-28 06:00:49 +00:00
claude[bot] 0ff908690a docs: review document for #684
Issue: #684
User-Visible: no
2026-09-28 06:00:47 +00:00
Claudeandclaude[bot] e79f3cbfbb docs: Esc and the tray's Reset finish an open Walls chain
UX-MODES.md and TOUCH-SUPPORT.md said Reset (and, by omission, Esc) do
not finish an open Walls chain. The code finishes it on both
(houseplan-card.ts Escape handler, the tray's btn.reset), as does
WALL-THICKNESS.md §11 "Finishing a chain". Both documents now list the
same finish actions and point to §11; re-selecting Walls, pan, pinch,
a second pointer and pointer cancellation stay non-finish actions.

Issue: #684
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 05:52:30 +00:00
claude[bot] ffcdb2945f docs(reviews): индекс после сдвига каталога (#686)
Issue: #686
User-Visible: no
2026-09-28 05:52:13 +00:00
claude[bot] 67ac9981cf docs: review document for #686
Issue: #686
User-Visible: no
2026-09-28 05:52:09 +00:00
Claude c455e7c82d fix(stairs): лестница-переход без рамки фокуса (#686)
Решение владельца 2026-09-28: «убрать рамку фокуса полностью». Навигационная
лестница View — фокусируемый `<g role="link" tabindex="0">` (#676), а своих
стилей фокуса у `.hp-stair` не было, поэтому браузер рисовал чёрно-белое
кольцо по прямоугольнику символа. Теперь `.hp-stair:focus` и
`:focus-visible` — `outline: none`: ни кольца браузера, ни замены. Лестница
остаётся в порядке Tab, Enter/Пробел по-прежнему переходят; hover и
выделение в редакторе не тронуты.

Свидетель — `demo/smoke_stairs.mjs`: снимок области лестницы с запасом на
кольцо без фокуса и с клавиатурным фокусом (`:focus-visible`) обязан совпасть
байт в байт; Enter на сфокусированной лестнице переводит на целевой этаж.
Без правила смок красный (`focusedStairPaintsNoFrame`). STAIRS.md и оба
changelog.

Issue: #686
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 08:28:00 +03:00
Sergey Matyunin 03a3dd9916 Release v1.78.0-beta.7 candidate
Issue: #679
Issue: #680
Issue: #681
Issue: #682
Issue: #683
User-Visible: yes
Release: v1.78.0-beta.7
2026-09-28 07:45:07 +03:00
claude[bot] 6d8d077340 docs(reviews): индекс после сдвига каталога (#682)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #682
User-Visible: no
2026-09-27 22:30:10 +00:00
claude[bot] 0dac3488fb docs: review document for #682
Issue: #682
User-Visible: no
2026-09-27 22:30:07 +00:00
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00
claude[bot] 65b3cb25f7 docs: review document for #682
Issue: #682
User-Visible: no
2026-09-27 22:10:47 +00:00
Claudeandclaude[bot] cca9bc856b docs(reviews): документы ревью линий до v1.77.0 — в legacy/reviews (#682)
Волна 5 эпика #674, перенос документов ревью (класс C), сделан
инструментом: `node scripts/reviews-archive.mjs --through=v1.77.0 --apply`.
965 документов 332 задач разложены по `legacy/reviews/<тег>/` (v1.63.0 …
v1.77.0) — 330 задач доказаны трейлерами своей линии, два документа без
трейлера (#68, #94, работа до правила трейлеров) — по линии, где их
добавили. В `docs/reviews/` остались 154 документа задач с трейлером после
v1.77.0 — текущая линия v1.78.0 — и INDEX.md, пересобранный тем же
построителем. История не переписана: SHA-якоря «Материала раунда» живы.
Счёт раундов (`reviewRounds` по живому каталогу и архиву) совпадает до и
после переноса: 694 пары «этап:задача». Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00
Claudeandclaude[bot] d4a672c715 feat(tools): архив документов ревью выпущенных линий (#682)
Волна 5 эпика #674, инструментальная часть (класс B).
`scripts/reviews-archive.mjs --through=vX.Y.Z` печатает план переноса
документов ревью в `legacy/reviews/<тег>/`, `--apply` делает `git mv` и
пересобирает `docs/reviews/INDEX.md`. Членство — трейлеры `Issue: #NN` в
диапазоне линии, как у манифеста беты (#547) и ревью линии (#638). Правила —
в чистой `archivePlan`: задача уходит в последнюю свою линию; задача с
трейлером после тега остаётся целиком (её раунды ссылаются на прошлые);
закрытая без выпуска уходит с линией, где лёг её документ; документ задачи
без трейлера — с линией, где его добавили; RELEASE-REVIEW — в каталог
своего тега; чужие имена не трогаются. План по v1.77.0: 965 документов
332 задач, 154 остаются в открытой линии.

`legacy/` — класс C в process-gate. Сравнения деревьев с якорем вердикта
(`review-doc-guard.mjs` #499, `task-packet.mjs`) не видят переноса в
`legacy/reviews/`. `process-metrics.mjs` считает раунды по живому каталогу и
архиву. Порог «>900 документов» в тесте индекса снят: в каталоге остаётся
текущая линия. PROCESS.md §2.10 уточнён (правила членства, пустая очередь
S7, ревью линии до переноса), в DEVELOPMENT › Release — шаг чеклиста.
Юнит-тесты и два мутанта (`reviews-archive-moves-open-line-issue`,
`reviews-archive-first-line-wins`).

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00
claude[bot] c64a1ab7d5 docs(reviews): индекс после сдвига каталога (#683)
Issue: #683
User-Visible: no
2026-09-27 22:03:08 +00:00
claude[bot] d460b31120 docs: review document for #683
Issue: #683
User-Visible: no
2026-09-27 22:03:05 +00:00
Matysh f880db0338 docs: обновить fingerprint после golden-фикстуры (#683)
Все 11 документационных кадров попиксельно совпали; изменён только отпечаток исходников.

Issue: #683
User-Visible: no
2026-09-28 00:02:16 +03:00
Matysh 2f86266f60 chore(golden): принять пользовательский стиль лестницы (#683)
Каноническая WSL-съёмка изменила только четыре объявленные сцены лестниц; остальные 180 сцен совпали.

Issue: #683
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed-Local: sha256:a56c54ffef7cd397d89f9eb895937c9af4c3d053d1289ee5c2cf7e9ecb8dd42b
2026-09-27 23:59:01 +03:00
Sergey Matyunin d5f3e17f24 test(golden): показать пользовательский стиль лестницы (#683)
Issue: #683
User-Visible: no
2026-09-27 23:52:34 +03:00
claude[bot] dd365cff45 docs: review document for #683
Issue: #683
User-Visible: no
2026-09-27 20:51:06 +00:00
Matysh 20d2863eea docs: обновить fingerprint скриншотов для лестниц (#683)
Все 11 документационных кадров попиксельно совпали; изменён только отпечаток исходников.

Issue: #683
User-Visible: no
2026-09-27 23:14:02 +03:00
Matysh 2c9ff32dc6 chore(golden): обновить аттестацию после ребейза (#683)
Все 184 сцены совпали пиксель-в-пиксель; обновлён только паспорт актуального дерева после переноса на dev.

Issue: #683
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed-Local: sha256:0fa156017baaeb4bc9c28de509d90ae04cf9405750a95f866fdc4d06df3c8585
2026-09-27 23:10:49 +03:00
Sergey Matyunin 1a7b1dfceb fix: синхронизировать манифест схемы лестниц (#683)
Issue: #683
User-Visible: no
2026-09-27 23:04:28 +03:00
MatyshandSergey Matyunin 64b544f168 chore(golden): принять четыре сцены лестниц (#683)
Каноническая WSL-съёмка изменила только четыре объявленные сцены лестниц; остальные 180 сцен совпали.

Issue: #683
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed-Local: sha256:d6b6c013e9be0dda05a7a1615b48dcfb0d5d0a8312415df2e0e13aef47f281bc
2026-09-27 23:04:28 +03:00
Sergey Matyunin 5b5b808184 feat: настроить визуальный стиль лестниц (#683)
Issue: #683
User-Visible: yes
2026-09-27 23:04:28 +03:00
claude[bot]andSergey Matyunin cf01489fa9 docs: review document for #683
Issue: #683
User-Visible: no
2026-09-27 23:04:09 +03:00
claude[bot] dd3a50426d docs(reviews): индекс после сдвига каталога (#680)
Issue: #680
User-Visible: no
2026-09-27 20:01:55 +00:00
claude[bot] bfcd9f1f15 docs: review document for #680
Issue: #680
User-Visible: no
2026-09-27 20:01:52 +00:00
Claude 72ce15bfa1 docs(hygiene): таблица владельцев файлов — в ARCHITECTURE, не во входе (#680)
Предыдущий коммит перенёс таблицу классификации файлов при сборке
(HP-1465-01) из ARCHITECTURE в SCOPE.md › Standing rule, а SCOPE — первый файл
обоих маршрутов входа: +211 слов маршрута автора и ревьюера — ровно перенос,
которого задача не допускает. Правило остаётся в SCOPE, таблица — в
ARCHITECTURE › Integration WS API › Files. entry-cost: автор 5 196, ревьюер
4 074.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:35:02 +03:00
Claude fdde46541e test(mutation): мутант бюджета входа автора превышает бюджет сам (#680)
После волны 3 маршрут автора — 5,4 тыс. слов из 12 тыс.: мутант
`entry-cost-author-route-over-budget` добавлял 2 000 слов и оставался
зелёным. Теперь патч сам больше бюджета (12 001 слово), и свидетель краснеет
независимо от длины остального маршрута.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:04 +03:00
Claude 4bc3e9baa6 docs(hygiene): ARCHITECTURE.md — карта, а не хроника (#680)
Волна 3 эпика #674. ARCHITECTURE.md 2330 → 653 строки: система координат
прототипа 1489×1053, разделы Hidden Isometric Stage 2/4 (текущее — ISOMETRIC.md,
история — ADR 122/570), хроники «Additions v1.28–v1.41» и «Audit follow-ups»
сняты; подсистемы — короткое описание со ссылкой на канонический документ.
Устаревшее исправлено по коду: дерево Layout (store.py, frontend_registration,
весь список модулей), хранилища (config, layout, virtual_lights, trails),
ленивые локали de и fr, таблица WS API (31 + 3 команды, #256-проекция,
space/delete, files/cleanup без keep, контент через /api/houseplan/content),
DevItem без несуществующих полей, отказ help/feedback по support_api.

Всё ещё верное и не записанное в другом месте перенесено в канонические
документы: DEVICE-PRESENTATION (заметки реализации, Action authority, черновик
диалога), RADAR (карта реализации), VACUUM (владение кодом), CANVAS (icon_size,
--hp-cell-visual-scale, барьер записи координат), FILTERING (#44, каталог),
DECOR-EDITOR §7 (инварианты бэкенда ассетов), WALL-THICKNESS (§1 идентичность
и нулевые стены, §2 кэши, §4 hover/туннели/острова, §6 удаление комнаты,
§9 failed-core, §11 завершение цепочки и комната по грани), ISOMETRIC
(isoPlaneMatrix, iso-overlays, створки, служебные атрибуты), LIGHT (Glow над
заливкой #55, формула и screen-смешение), CONFIG-COMPATIBILITY (манифест схемы
#33, Masonry-слоты #561, квадратный холст v1.48, устаревшие URL контента,
layout/set #356), SCOPE (таблица владельцев при сборке файлов), WARM-REMOUNT
§5 (холодная загрузка и визуальная непрерывность), UX-MODES (порядок
стартового пространства), PDF-EXPORT (граница реализации), FURNITURE (adopt,
BOOT_MAX_MS). Раздел TESTING «Backend quality gates (#42)» и строка
DEVELOPMENT о bundle-freshness — из того же разбора, в предыдущем коммите.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:04 +03:00
Claude 696f5a789f docs(hygiene): сократить вход агента, у правила — один дом (#680)
Волна 3 эпика #674. AGENTS.md 650 → 187 строк: карта пакета, маршрут чтения,
правило №1, классы и треки одной строкой со ссылками, трейлеры, рабочие
деревья, хендофф и ожидание вердикта; пересказы PROCESS.md — ссылками на
разделы. Неверный список «Gate jobs» снят (списки jobs не копируются в прозу,
шапка PROCESS.md). Правила, жившие только в AGENTS, получили дом: жёлтый
вердикт при выполненных AC — PROCESS §2.7; свежесть бандла, съёмка только в
Linux (#455, HP_ALLOW_FOREIGN_CAPTURE) и смоки из AC до S7 (#151) —
TESTING.md; причуда демо-стенда и среда-зависимый smoke_opening_measure —
DEVELOPMENT › Smoke tests; отказ публикации без `Release:` и при несвежем
отпечатке бандла, отмена Validate новым пушем, кандидат беты не
promotion-only, fail-closed реестра Labs — DEVELOPMENT; предупреждение и
ошибка свежести скриншотов — CONTRIBUTING.

PROCESS.md: §13 (внедрение с открытым ⏳), §14 (блок со ссылкой на
несуществующий docs/PROCESS.md) и §7.3 (история) удалены. Ссылки «§7.2» на
правило полного разбора после ребейза ведут в §2.10, на сверку SHA перед
выводом — в §2.7; то же в сообщениях scripts/branch-state.mjs,
merge-candidate.mjs, review-doc-guard.mjs, pre-push-gate.mjs, в промпте
_process.yml и TESTING.md. Число `any` в прозе → `node scripts/no-new-any.mjs
--total` (новый режим, юнит-тест; было «1034 в 49 файлах», сейчас 862 в 52),
дата-число замороженного списка якорей монолита снято. Устаревшая команда
пересъёмки скриншотов в §8 заменена ссылкой на действующий путь.

STATUS.md 113 → 61 строка: сгенерированный снимок, текущий цикл и девять
строк решений; Workflow, CI, Toolchain, Tests, Scope, open items и политика
документации — ссылками (PROCESS §2.6, DEVELOPMENT › Release, TESTING);
локали en/ru/de/fr; закрытые «coverage, mypy strict» сняты.

DEVELOPMENT.md: file-sync и «Reproducible scripts» (прототип) удалены;
раздел Release — единственный дом релизной механики: введение, правила
тела стабильного релиза (#328, release:notes), шаг continuity:screencast,
источники версии по release-contract. CONTRIBUTING: ссылка на Release вместо
пересказа, замеры клона без чисел. TESTING: any-гейт — ссылкой на PROCESS §8.

entry-cost: автор 11 125 → 5 407 слов, ревьюер 8 464 → 4 285.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:03 +03:00
claude[bot] 972ff701d1 docs(reviews): индекс после сдвига каталога (#681)
Issue: #681
User-Visible: no
2026-09-27 19:11:40 +00:00
claude[bot] a57ba2e264 docs: review document for #681
Issue: #681
User-Visible: no
2026-09-27 19:11:36 +00:00
Claude 708ed850f3 docs(hygiene): снять чек-листы testing-notes и дизайн-материалы выпущенных задач (#681)
Волна 4 эпика #674.

docs/testing-notes/: восемь ручных чек-листов по поверхностям и индекс
удалены — ни одного отмеченного пункта, ручной фазы в PROCESS.md §2 нет.
Правило #650 (пустое совпадение --test-name-pattern) перенесено в
TESTING.md; пункты [manual] без автоматического свидетеля сведены в раздел
«Чего не проверяет автоматика» (реальный HA, сенсорное устройство, ресурсы
сервера, несколько клиентов, визуальная оценка, пользовательское
содержимое). В TESTING.md снят блок чек-листов v1.43–1.44 и приложения по
issue в разделе golden (#197/#249/#272/#275/#288/#261) — сцены объявлены в
demo/golden/matrix.mjs. Остался реестр браузерных гвардов #659
(mutation-browser-guards.md). test/testing-notes-index.test.mjs →
test/testing-doc.test.mjs: лимит 800 строк, правила #85, раздел ручных
проверок и живые ссылки TESTING.md; каталог testing-notes содержит только
реестр. Мутант testing-notes-index-drops-section (удалял строку индекса) →
testing-doc-drops-manual-section. golden-matrix: копия 67 id сцен #242/#250
в чек-листе снята, список и способ его измерения — в demo/golden/matrix.mjs.

docs/design/505-summary-panel удалён вместе с
demo/capture_summary_panel_505.mjs и маршрутом /reference/ фикстуры
диалога. docs/design/600-settings-dialogs: reference/, screenshots/,
pairs/, ARCHIVE-README, ISSUE-FORM, OPEN-POINTS удалены; SPEC,
IMPLEMENTATION-GUIDE, field-maps, ACCEPTANCE остаются; вывод
capture_design_pairs_600.mjs и verify_ha_form_shell_609.mjs --capture —
в artifacts/. docs/design: 68 файлов / 4,08 МБ → 13 / 0,70 МБ.
README-ha-dialog-505.md → README-ha-dialog.md (путь в release-review.yml);
demo/guard/README.md: запись гварда — в verify-guard.mjs, не в README.

docs/design/649-25d-stage6 не тронут — пункт после стабильного v1.78.0.

Issue: #681
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 21:27:57 +03:00
claude[bot] c8fc8838d8 docs(reviews): индекс после сдвига каталога (#679)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #679
User-Visible: no
2026-09-27 18:23:50 +00:00
claude[bot] f4fc3bd680 docs: review document for #679
Issue: #679
User-Visible: no
2026-09-27 18:23:47 +00:00
Claudeandclaude[bot] 030780386c docs(hygiene): назвать тест сверки схемы в реестре совместимости (#679)
Правка шапки CONFIG-COMPATIBILITY.md в предыдущем коммите сослалась на
«тесты #33» без имени — ровно та форма, которую эпик #674 снимает у
матрицы света. Теперь названы файл и что он держит: согласие enum бэкенда и
фронтенда через allow-list и разрешимость каждой записи реестра в путь
манифеста схемы.

Issue: #679
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 18:07:37 +00:00
Claudeandclaude[bot] 5a258f3128 docs(hygiene): свести дубли документов подсистем и снять устаревшее (#679)
Волна 2 эпика #674 — у каждого правила один дом, остальные места ссылаются.

DECOR-EDITOR.md ← BACKDROP.md + LIVE-TEXT.md: один документ с нумерованными
разделами (§3 подложка, §5 текст с живыми значениями), на которые теперь
указывают комментарии кода вместо несуществовавших «BACKDROP §2/§3»;
исправлено утверждение, что space-card не рисует декор (он рисует подложку
и картинки декора, но не фигуры, мебель и текст). LIGHT.md ← матрица
настроек света (перевод, тест назван явно: test/devices.test.mjs «issues
84/88»). DEVICE-PRESENTATION.md ← правила «что показывает маркер» из
FILTERING.md (порядок cover → light sources → device role, шторы,
медиаплееры); «в одном pull request» → «в одном коммите». CANVAS.md: §9.5
«Оптимизировать планы» → CONFIG-COMPATIBILITY.md, overlay и планарные грани
Walls → WALL-THICKNESS.md §10–11, таблицы «было/стало» сняты. TESTING-DEMO.md
→ demo/stand/README.md: карта демо-дома и «чего на стенде нет», ручной
чек-лист снят (ручной фазы в процессе нет). ISOMETRIC.md — только текущее;
история Stage 2/4 — docs/adr/570-isometric-stage4-visual-handoff.md.
SUN.md: удалённый контракт фона снят, правило бумаги — в текущем разделе.
UX-MODES.md: декор над заливками, а не «под комнатами»; «hidden isometric»;
follow-up из #3 — все выпущены. Шапки VACUUM, WARM-REMOUNT («Выровнять всё
по сетке» → «Оптимизировать планы»), WALL-THICKNESS, STYLING-HOOKS,
CONFIG-COMPATIBILITY (#33), PDF-EXPORT — без устаревших статусов и планов.
README EN/RU: абзац про пересъёмку скриншотов → CONTRIBUTING.md, RADAR и
PDF-EXPORT в списке документации, RU догнал EN (2.5D, повторное
использование загруженного изображения, STAIRS). Один список канонических
документов подсистем в AGENTS.md и промпте ревьюера (_process.yml).
WALL-THICKNESS.md ссылается на ADR 282.

Правки src/** и validation.py — только пути документов в комментариях.

Issue: #679
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 18:07:37 +00:00
Claude c392ad3a9c ci: mirror thin workflow callers in main after #658
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 3m1s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 3m24s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 3m31s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 3m33s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 1m50s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 1m49s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 1m51s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 1m54s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 27s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 47s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 37s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 32s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m13s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 2m54s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 1m15s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 1m14s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 7m1s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 19s
Расписания исполняются из ветки по умолчанию: три тонких вызывающих файла
обязаны совпадать с dev. Зеркало dev@f1a87fba: cron ушли с круглых минут
(ночь 02:17, мутанты 00:43, метрики понедельника 05:23).

Issue: #658
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 14:20:42 +03:00
Claude dc72141af6 ci: mirror thin workflow callers in main after #623
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 1m47s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 1m45s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 2m8s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 2m3s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 1m37s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 1m39s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 37s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 1m54s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 1m55s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 31s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 1m32s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 25s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 16s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 46s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 39s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 37s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 7m13s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 22s
Шесть файлов, которые GitHub исполняет из ветки по умолчанию, стали тонкими
вызывающими: тела живут в dev (`_*.yml`) и вызываются `@dev`. Это последнее
ручное зеркало тел; дальше тонкие файлы меняются только вместе с триггерами
или потолком прав.

Issue: #623
User-Visible: no
2026-09-24 10:59:05 +03:00
Claude 8741f3f6d7 ci: mirror process.yml in main after #643
Конвейер исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev: ребейз перед ревью пересобирает docs/reviews/INDEX.md вместо отказа.

Issue: #643
User-Visible: no
2026-09-24 10:08:20 +03:00
Claude be54edf988 ci: mirror mutation-gate.yml in main after #620
Ночной реестр исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev@ee4bbf8f: пропуск ночи по неизменённому дереву, окружение шарда по плану.

Issue: #620
User-Visible: no
2026-09-24 09:24:21 +03:00
Claude 7752329d71 ci: mirror process.yml in main after #634
Конвейер исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev@7dc75972: промпт ревьюера ссылается на docs/process/REVIEWER.md.

Issue: #634
User-Visible: no
2026-09-24 07:51:40 +03:00
Claude 093ea906ae ci: mirror process.yml in main after #635
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 1m51s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 2m6s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 1m43s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 1m44s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 2m14s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 2m12s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 2m10s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 2m14s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 35s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 1m10s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 1m39s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 18s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 36s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m11s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 42s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 36s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 38s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 21s
Issue: #635
User-Visible: no
2026-09-23 18:26:47 +03:00
Claude 0c93344913 ci: mirror process.yml in main (#621)
Конвейер исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev@50e67c09: листинг docs/reviews через Git Trees API.

Issue: #621
User-Visible: no
2026-09-23 13:21:59 +03:00
Claude eecb5291d0 ci: зеркало еженедельных метрик процесса в main (#637)
Issue: #637
User-Visible: no
2026-09-23 11:58:52 +03:00
Claude 58ba44dbac ci: mirror review-resume workflows in main (#636)
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 1m55s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 1m57s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 2m6s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 42s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 1m35s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 1m51s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 1m50s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 1m56s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 1m53s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 45s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 24s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Failing after 24s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 50s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 20s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 46s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 35s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 6m4s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 23s
Issue: #636
User-Visible: no
2026-09-23 09:43:01 +03:00
1505 changed files with 13865 additions and 19439 deletions
+12 -7
View File
@@ -487,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). Вердикт применим только к
@@ -1015,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): найди вердикт и материал
@@ -1534,7 +1539,7 @@ jobs:
# Ревью идёт десятки минут, а dev за это время двигается (28 августа —
# четыре раза за день). Вердикт при этом вынесен по дереву, которое уже не
# совпадает с вершиной линии, и слияние приведёт ветку к dev — то есть в
# dev уедет код, отличный от прочитанного (§7.2). Молчать об этом нельзя,
# dev уедет код, отличный от прочитанного (§2.10). Молчать об этом нельзя,
# но и шуметь на каждом прогоне ни к чему: строка появляется только когда
# dev действительно ушёл и вердикт зелёный, то есть слияние вот-вот
# случится (#364).
@@ -1553,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. Значит слияние обязано произойти
# ДО метки, иначе она врёт в промежутке.
+1 -1
View File
@@ -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, данные/права, геометрия,
+112 -572
View File
@@ -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. The synthetic home is fully fictional (no real home data in public materials); the launcher is `demo/serve.mjs`, the stand copy of the bundle comes from `npm run bundle:sync`, golden scenes live in `demo/golden/` (its README), performance smokes in `demo/performance/`, the guard suite in `demo/guard/` and the live stand seed in `demo/stand/`.
- **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`.
+22 -15
View File
@@ -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.
+40 -102
View File
@@ -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 Код-ревью
@@ -273,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,
зафиксированным в начале разбора: во время ревью в ветку может прилететь
@@ -286,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`;
@@ -358,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, чьё доказательство дельта задевает;
@@ -395,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), смена контракта поведения, задета
новая подсистема, либо объём дельты сопоставим с исходной задачей.
Сокращается объём **разбора, а не строгость**: правка по замечанию способна
@@ -699,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. Гейты
@@ -734,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: <конкретная причина>`; голый маркер и причины вида
@@ -776,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`,
@@ -1143,7 +1148,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
- ветка уже содержит весь `dev` — ничего;
- отстала и ребейзится чисто — ребейз, `push --force-with-lease`, ревью по
приведённому состоянию. Факт ребейза передаётся в промпт, чтобы сработало
правило §7.2 о полном разборе вместо дельты;
правило §2.10 о полном разборе вместо дельты;
- конфликт — возврат в `S6-in-progress` **до** запуска ревью. Цикл при этом не
расходуется: код никто не читал, вердикта нет.
@@ -1165,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` двинулся снова
@@ -1215,7 +1220,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
повторно **без вызова модели**, если последний документ этапа несёт записанный
конвейером вердикт `green` с High 0 и дерево материала не изменилось ни в одном
файле вне `docs/reviews/**` (сравнивает `git diff` по содержимому). Ребейз, правка
теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §7.2 не
теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §2.10 не
ослабляется, оно просто не касается дерева, которое уже читали.
Цикл считается **по этапу**: вердикт по ТЗ не расходует бюджет код-ревью. Раньше
@@ -1236,7 +1241,7 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
- issue создан в **той же сессии до коммита**, метка `hotfix`;
- ТЗ «как сделано» + раздел «почему нельзя было ждать»;
- в течение 24 часов задача ретроспективно проходит код-ревью;
- аварийность названа явно в релизном хендоффе (действующее правило `AGENTS.md`).
- аварийность названа явно в релизном хендоффе.
### 11.3 Гигиена репозитория
@@ -1378,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 закрывает релиз-менеджер после выпуска беты, не исполнитель.
```
+2 -5
View File
@@ -189,6 +189,8 @@ concurrent clients, but avoid editing the same object in two browsers at once.
- [Stairs and floor links](docs/STAIRS.md)
- [Background editor](docs/DECOR-EDITOR.md)
- [Robot vacuums](docs/VACUUM.md)
- [Presence radars](docs/RADAR.md)
- [PDF export](docs/PDF-EXPORT.md)
- [Touch support](docs/TOUCH-SUPPORT.md)
<!-- docs-section: support -->
@@ -201,9 +203,4 @@ concurrent clients, but avoid editing the same object in two browsers at once.
Include the version, browser, logs and reproduction steps; private entity IDs
may be replaced with fictional ones.
Documentation screenshots are produced by the reproducible
`npm run build && node demo/docs/capture.mjs` command using synthetic data only. Scenario version,
source fingerprint and every image hash are recorded in the
[screenshot index](docs/images/screenshots.json).
License: [MIT](LICENSE).
+9 -7
View File
@@ -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).
+1 -1
View File
@@ -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.6"
VERSION = "1.78.0"
DECOR_ASSETS_API_VERSION = 1
SUMMARY_PANEL_API_VERSION = 1
@@ -1,436 +1,436 @@
{
"schema": 1,
"fingerprint": "51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",
"fingerprint": "a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299153,
"initialViewGzipBytes": 300323,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 300675,
"initialPanelGzipBytes": 301845,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2324,
"initialPanelOnlyGzipBytes": 2325,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.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": 456031,
"lazyGzipBytes": 456389,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.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": 237970,
"lazyEditorGzipBytes": 238192,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/namespace-language-CIeeXPH2.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": 27944,
"lazyOnboardingGzipBytes": 27950,
"lazyLocaleFiles": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/fr-BLCP2_-N.js"
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/fr-CpDKyK4Z.js"
],
"lazyLocaleGzipBytes": 56294,
"lazyLocaleGzipBytes": 56330,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-SCzI9z9k.js"
"houseplan-assets/iso-scene-render-BxXtKC5L.js"
],
"lazyIsometricGzipBytes": 17882,
"lazyIsometricGzipBytes": 17885,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js"
],
"lazyFurnitureArtGzipBytes": 16941,
"lazyFurnitureArtGzipBytes": 16940,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/pdf-export-CGU5_sl_.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/pdf-export-DL7VmwpB.js"
],
"lazyPdfGzipBytes": 126914,
"lazyPdfGzipBytes": 126938,
"lazyNamespaceLocaleFiles": [
"houseplan-assets/settings-de-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js",
"houseplan-assets/support-de-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.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": 21037,
"lazyNamespaceLocaleGzipBytes": 21057,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CmogasSW.js",
"sha256": "aea6305d5506f017f2b0f8239cce47b10f2e02136b53b3002e2f708deab95444",
"path": "houseplan-assets/backdrop-pick-WDIF6iao.js",
"sha256": "5f6dcedf274594075d8acd31295d8c23f6d1408c890c086825db6bcbaa6c9b21",
"rawBytes": 54017,
"gzipBytes": 17574,
"gzipBytes": 17576,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.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-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js"
"houseplan-assets/settings-de-DfJ7EKF9.js",
"houseplan-assets/settings-fr-Bl79E-6s.js",
"houseplan-assets/settings-ru-CkNFmChi.js"
]
},
{
"path": "houseplan-assets/de-D7PZcyoc.js",
"sha256": "5e425bd4558bffbf4ad81d3a51e3ecd9fe7f76697482131b00ddeae65c0b37a4",
"rawBytes": 94126,
"gzipBytes": 28375,
"path": "houseplan-assets/de-E17v3gXt.js",
"sha256": "ed10d6c5e7a82b275c9422be67c2213219a75dba9aab0ddb3e98813249cbd1d0",
"rawBytes": 94196,
"gzipBytes": 28393,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CKG7G85d.js",
"sha256": "db6d125e73e1d2c8535645c576f7df912badd35bb93477d2a38537203c0dbba5",
"path": "houseplan-assets/editor-CbeYCXUv.js",
"sha256": "75887638c4d76040651a3cf52c56de10dc19e0cbc7583ee169d9b20cc5987788",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"sha256": "4252673f16e0382d3dd9ec967c8df782b2a315bb0658f05ad246f70e77aad222",
"path": "houseplan-assets/form-kit.styles-v5DlO12L.js",
"sha256": "cdab9e5e612be9b98c49193a2a6287080c13f62aaefa9a28b69bac94dd934012",
"rawBytes": 25600,
"gzipBytes": 5055,
"gzipBytes": 5056,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-BLCP2_-N.js",
"sha256": "b6f1cd483e2f0bb3c30ce2089b5e52fb65578e656496b2b9623b877b32339fa6",
"rawBytes": 96530,
"gzipBytes": 27919,
"path": "houseplan-assets/fr-CpDKyK4Z.js",
"sha256": "ee9fe5934eea69e549ff48d2e3fdabd7c20e0d9906a6f4623314f6eea48bf269",
"rawBytes": 96627,
"gzipBytes": 27937,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"sha256": "1152cf5d82e01ae897ef187661978a5d0c3a7bf49f8655c96ddaad564c191c29",
"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-BVZ7SgFj.js",
"sha256": "de63fea2ef07a50481acafb3236f8f02962895cc630b026910f76b2be4c431fd",
"rawBytes": 1055543,
"gzipBytes": 298351,
"path": "houseplan-assets/houseplan-card-BYwTiVnu.js",
"sha256": "055b6f8ba3a472be163a15ef6e2348727b5b03e3660b9362f3aed21d7be90426",
"rawBytes": 1059790,
"gzipBytes": 299520,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.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-CpIQitHC.js",
"sha256": "3fb964d9241252e3b00bedcc0c89f994aec183b3f657b6af38496b1f803db253",
"rawBytes": 687631,
"gzipBytes": 192112,
"path": "houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"sha256": "f25cbcb0453279e3ac64eda67df52af100d96c54bb93cb657bc08c355e0484ed",
"rawBytes": 688848,
"gzipBytes": 192327,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.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-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.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-BzvIlGC6.js",
"sha256": "2fcbe1523d5e570804a65c9138d7d2b41c951320af19a36844f9536b32218564",
"path": "houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"sha256": "4ea3584c8b2bfc86f96c587549ddce49eaf0b918923df2df8e98c9c0a548f835",
"rawBytes": 15886,
"gzipBytes": 4906,
"gzipBytes": 4908,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.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-CsVfJvR7.js",
"sha256": "bd59aa84c7afc96a1f50abaf8205bd745798264f27114a40cd7b334aabe1fd14",
"path": "houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"sha256": "2a0f9af108b8a39889f90d7119d8e7789716c9960ea8e28af38ada07cdc91a8c",
"rawBytes": 10958,
"gzipBytes": 3767,
"gzipBytes": 3765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.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-SCzI9z9k.js",
"sha256": "cf356172a58c90e29476142476c4d625b49d13c7414fbce151327b8b3626d40f",
"path": "houseplan-assets/iso-scene-render-BxXtKC5L.js",
"sha256": "4150eb9120fdaafa745761242ff3285056ceaccd7b016a16364574b90e3e2cfe",
"rawBytes": 52970,
"gzipBytes": 17882,
"gzipBytes": 17885,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"sha256": "a395a4dcb26e33285521d157e16bacff01f26da5f46158b30e6e25c48679644e",
"rawBytes": 8216,
"gzipBytes": 3208,
"path": "houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"sha256": "66e70baa00f4c9470ac9a37a229652cf5c5585caa2364de4d467a1c2ebe24358",
"rawBytes": 8370,
"gzipBytes": 3280,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/namespace-language-CIeeXPH2.js",
"sha256": "4bbd058806d1870d8d6b109f1a7c1a29e86b0ee2188a46441d38ba6719795964",
"path": "houseplan-assets/namespace-language-zIuNvgGV.js",
"sha256": "244bce2d5211106846215be6a9d3dacda4c910d06020144f9ffd5e432c5e8c00",
"rawBytes": 675,
"gzipBytes": 409,
"gzipBytes": 410,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CGU5_sl_.js",
"sha256": "0b1e4968ecf729dc267e03d63062bba53490e737635d9f3f1681a0320768e6d4",
"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-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-de-Crekk5Hk.js",
"sha256": "a769daa4cf5674115e7ed3172992b247e56f4ce72f883af46921dbb52f8512ad",
"path": "houseplan-assets/settings-de-DfJ7EKF9.js",
"sha256": "0580b9f6191edb664c21dee21e5263489eaf22876bcad67ef12e440abc03e28a",
"rawBytes": 7378,
"gzipBytes": 2879,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-Bl79E-6s.js",
"sha256": "f1bde9db7fe884aabd25130f783a84d8fe27bc9553923c17bcb9b82ee3976475",
"rawBytes": 7620,
"gzipBytes": 2842,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-CkNFmChi.js",
"sha256": "94f96c4a15a3a82ca8be3066629734379dd14eb2ff9e8f6acf369da2378676cb",
"rawBytes": 10304,
"gzipBytes": 3325,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-C_vpFucN.js",
"sha256": "d45dbdfa17a1da9f45bfbca6d6c3cc98849fd5ebcc06c8f72fd6310b04b93aad",
"rawBytes": 14862,
"gzipBytes": 3425,
"isEntry": false,
"imports": [
"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-D47J-OjO.js",
"sha256": "909d2195fcfca30c2f1279e07e2d7506d470c3daec6fe864c97f7ecc4101e33d",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
"sha256": "e44860a1af9ec7ffc7ac289c8c76ed1d0dcf0177bd91bbfe1981fdd0d4a62c46",
"rawBytes": 72959,
"gzipBytes": 20242,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-C_vpFucN.js",
"houseplan-assets/summary-panel-metrics-D47J-OjO.js"
]
},
{
"path": "houseplan-assets/support-de-DZoi-fxk.js",
"sha256": "f01d2d00a169519f5f402e99e9c85c9f21a23aeee2a9605d4444910f3250d6de",
"rawBytes": 6167,
"gzipBytes": 2396,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-Daosy89Q.js",
"sha256": "e503a1b4e1b1ed31d8fa09bfe0cdc069a82c1d1627d40407472b336fbc98ccf5",
"rawBytes": 6333,
"gzipBytes": 2432,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-e8-BrqtH.js",
"sha256": "76742313c4b510163c380f8fd9c7fcaf1cc7f8bde6a41bcb0da48621a7c3a3b3",
"rawBytes": 8376,
"gzipBytes": 2876,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-BzCuGnet.js",
"sha256": "40a596406b0bafbc3f2d9a54b12b9ac9e1713f939f98a630c0a576b53780d255",
"rawBytes": 7620,
"gzipBytes": 2839,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-BxY9uxyq.js",
"sha256": "929c462c195a7cf42c8cb2d4b558b4ae5ab1508eeafe4fb6a9e890f3650464ff",
"rawBytes": 10304,
"gzipBytes": 3323,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"sha256": "388fb3fcd97490eee5cdb7140953f7b421fcd06bee24c7f19e0e79d24b777bde",
"rawBytes": 14862,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js",
"sha256": "6c370c9858be82bdc1e77f77d598147be7673ac64656f533a62c0b15f6cfa5a9",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"sha256": "45c6b69dea88a448e81976da8cad5656bc7ee26ee49b3f9685d8bcba62d845a3",
"rawBytes": 72959,
"gzipBytes": 20243,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js"
]
},
{
"path": "houseplan-assets/support-de-9JIBMnMv.js",
"sha256": "83f703aba89eccb9e1b8ebf5e5b78d60521406a6a1ae7e4cc3a852a68f74b1c9",
"rawBytes": 6167,
"gzipBytes": 2395,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-B7V5UpZx.js",
"sha256": "3515c3761d2870728cac7760d871ccd03d9468d5001e43670b14a5b64126f6d3",
"rawBytes": 6333,
"gzipBytes": 2429,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-DVgRGpdZ.js",
"sha256": "794b61c69025bbe3cee78266b8833a5c22094930bfbd419fdea43fc38ca2f8cf",
"rawBytes": 8376,
"gzipBytes": 2875,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-de-Eo3zRC2b.js",
"sha256": "b73933a557179215bdae75cc51b2713541a032ed6c0240b51599f4ab73bf0ebb",
"path": "houseplan-assets/topology-de-Cs36ouVE.js",
"sha256": "7c808f1139bb1c232627bfe884a579177ec653d16147d6b96066851893b312a4",
"rawBytes": 2698,
"gzipBytes": 1333,
"gzipBytes": 1337,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-fr-BZMZ_i-b.js",
"sha256": "341977cab7561948330323c3da99cde1c56f9a225f9f78c154ce37b6af9f6a0c",
"path": "houseplan-assets/topology-fr-B8yoJGvc.js",
"sha256": "6ee1530fd0bd7a8c476c79b83404c7d8a5b98cd0b86ee8a73048be75a70cc2c4",
"rawBytes": 2718,
"gzipBytes": 1353,
"gzipBytes": 1354,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-ru-MwP3wic3.js",
"sha256": "f49389bc3c849476d3b97d38493262f8eb7a10aa6432dd6d555578f486b21f4a",
"path": "houseplan-assets/topology-ru-BRKShmrQ.js",
"sha256": "7fc89542fa860318d459ce1467a5a8931f3214a9d17f57c177ba01f2cfbbd3e8",
"rawBytes": 3669,
"gzipBytes": 1614,
"gzipBytes": 1616,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-CjcVZzSn.js",
"sha256": "b0136228d2845e59ef35b87fff2e19d28609fcef222ad2f46d6c71f186a9ece2",
"path": "houseplan-assets/zigbee-topology-DOAnbvlu.js",
"sha256": "313f71677ba0739d9c978185c2136e73e8037b770eb3f9ef8561f692d654406f",
"rawBytes": 10958,
"gzipBytes": 4296,
"gzipBytes": 4299,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": [
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.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--7WS-Nzl.js",
"sha256": "25dd84923fd69b9a594b2b74a191f8e6dba398a757f8a05f3ea7c527bcf27399",
"path": "houseplan-assets/zigbee-topology-runtime-CKlry6R8.js",
"sha256": "39e4a250de0e67e80d8afc02c39c1a79e42d4b3e113107bb3817f4fa90cab1e2",
"rawBytes": 3983,
"gzipBytes": 1788,
"gzipBytes": 1789,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.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": "7348cd78a4e1e27962b597d63f8d877291176a296357a03568557dda09df0903",
"sha256": "d4b480db1327152db7b1459c45692ea6b72fb7ac3bccb525bc06adfe1aab3b8d",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "54a23573ba2306bb0734b8b0ab051555d3646f68a2d8ad90798d0e2c69cc0abd",
"sha256": "5aaa677a9c85f4df0f176be31a91ebb5022f5ce129774919269bb0409c310233",
"rawBytes": 6165,
"gzipBytes": 2324,
"gzipBytes": 2325,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{aD as e,l as o,aG as t,ev as s,A as a,ew as i,ex as l,ey as n,E as r,c}from"./houseplan-card-BVZ7SgFj.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);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{eA as e,eB as a,ex as n}from"./houseplan-card-BVZ7SgFj.js";function c(n,c){return new e([{code:"en",dictionary:n},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",console.warn,a)}function d(e){return function(e,a){return{state:n=>{const c=e.state(n);return"pending"!==c&&a.some(e=>"pending"===e.state(n))?"pending":c},dictionary:a=>e.dictionary(a),ensure:n=>Promise.all([e,...a].map(e=>e.ensure(n))).then(()=>{})}}(n,e)}export{c 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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{c as a,A as e}from"./houseplan-card-BVZ7SgFj.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-3QYyyR8a.js";import"./form-kit.styles-Ck5TDLDh.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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{eD as e,N as t,b2 as n,L as i,ae as o,ac as r,c6 as s,eE as a,c5 as c,b1 as l,b0 as d,b3 as u,eF as f,eG as m,c8 as g,eH as v}from"./houseplan-card-BVZ7SgFj.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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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 a="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";export{e as dictionary,a 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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{n as t,c as n}from"./zigbee-topology-CjcVZzSn.js";import{aM as e}from"./houseplan-card-BVZ7SgFj.js";import"./namespace-language-CIeeXPH2.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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";try{await import("./houseplan-assets/houseplan-card-BVZ7SgFj.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)}
File diff suppressed because one or more lines are too long
@@ -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 []
]
+1 -1
View File
@@ -20,5 +20,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.78.0-beta.6"
"version": "1.78.0"
}
@@ -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"))
+8 -4
View File
@@ -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
+3 -2
View File
@@ -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 = {
-317
View File
@@ -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;
}
+18 -14
View File
@@ -1,8 +1,8 @@
{
"schema": 3,
"matrixVersion": 66,
"acceptedAt": "2026-09-27T13:57:53.353Z",
"sourceFingerprint": "759c152cd8e59330f4a0efed0b464684aac9863b417653c26aca97595f977d72",
"matrixVersion": 67,
"acceptedAt": "2026-09-28T13:36:48.059Z",
"sourceFingerprint": "baa0806921c7f72959047c7d2bda343ef5e378c4e8d2a8e612f74b6af2e2c5b1",
"chromium": "151.0.7922.34",
"capturedOn": "linux",
"acceptedOn": "linux",
@@ -10,18 +10,18 @@
"platform": "linux",
"arch": "x64",
"chromium": "151.0.7922.34",
"buildFingerprint": "759c152cd8e59330f4a0efed0b464684aac9863b417653c26aca97595f977d72",
"buildFingerprint": "baa0806921c7f72959047c7d2bda343ef5e378c4e8d2a8e612f74b6af2e2c5b1",
"ci": {
"repository": "Matysh/houseplan-card",
"run": 36320878644,
"run": 36427848424,
"attempt": 1,
"sha": "193d7b0d3020cd69798fbe90c3aceb06dd221a45"
"sha": "c1289886175e165c5be09e4bd700a572529da234"
}
},
"localAttestation": null,
"foreignCapture": null,
"witnesses": {
"count": 131,
"count": 107,
"floor": 10
},
"scenarios": {
@@ -30,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": "17387bddf46a7506f7f578242161c91391fd15998aa68aa1e022073113d0c7ac",
"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",
@@ -69,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",
@@ -101,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",
@@ -160,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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+25
View File
@@ -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
+25 -4
View File
@@ -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 },
+3 -2
View File
@@ -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 записи не заменяет.
-124
View File
@@ -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.
+76
View File
@@ -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.
+1 -4
View File
@@ -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 -1
View File
@@ -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;
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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
+4 -1
View File
@@ -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(
+3 -1
View File
@@ -166,7 +166,9 @@ out.dayCycleOutlineFiltered = /drop-shadow/.test(safeDayStyle?.filter || '');
out.dayCycleOutlinePromoted = /filter/.test(safeDayStyle?.willChange || '');
out.dayCyclePaperStaysUnfiltered = (safeDayStyle?.paperFilter || 'none') === 'none'
&& !/filter/.test(safeDayStyle?.paperWillChange || '');
out.safePlanLayerIsExplicit = /transform/.test(safeDayStyle?.planWillChange || '');
// #689: the explicit scene layer is an opacity hint; a transform hint would
// freeze its raster scale after a zoom.
out.safePlanLayerIsExplicit = (safeDayStyle?.planWillChange || '') === 'opacity';
out.rasterTasksObserved = [...staticSamples, ...dayCycleSamples].every((sample) => sample.tasks > 0);
const staticMedianMs = median(staticSamples.map((sample) => sample.ms));
+236
View File
@@ -0,0 +1,236 @@
// #689: with the day/night background the plan layers must stay stage-sized at
// any zoom, and the scene must not keep a will-change: transform hint (its explicit layer is
// an opacity hint). Before
// the fix, CDP LayerTree measured (~460 %, DPR 2) plan-svg at 15.9× and the
// filtered outline at 13.2× the stage during a gesture, the outline at 12.8×
// at rest; at 800 % that is hundreds of MB of GPU memory and navigation
// flashed white. The same will-change: transform hint froze the raster scale:
// zooming from 100 % showed the 100 % raster stretched. Headless Chromium can
// see neither the blur nor the flashes (owner-verified in real Chrome), so this
// witness measures the state that causes them: layer sizes and the hint.
import { launch, checkAll, finish } from './serve.mjs';
import { installHpTestOnPage } from './helpers/hp-test.mjs';
const { page, browser } = await launch({ width: 820, height: 760 }, 2);
const cdp = await page.context().newCDPSession(page);
let latestLayers = [];
cdp.on('LayerTree.layerTreeDidChange', ({ layers }) => { latestLayers = layers; });
await cdp.send('LayerTree.enable');
const frames = [];
cdp.on('Page.screencastFrame', (event) => {
if (frames.length < 120) frames.push(event.data);
void cdp.send('Page.screencastFrameAck', { sessionId: event.sessionId });
});
const settle = async () => {
await page.waitForTimeout(1200);
await page.evaluate(async () => {
await window.__card.updateComplete;
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
});
};
const dayNight = async () => {
// A dark custom room fill keeps the near-white frame sample meaningful at
// 800 %, where the demo's own light paper would otherwise fill the stage.
await page.evaluate(() => window.__hpTest.setServerConfig((cfg) => ({
...cfg,
settings: { ...(cfg.settings || {}), bg_mode: 'daynight', glow_enabled: false },
spaces: cfg.spaces.map((space) => ({
...space, settings: { ...(space.settings || {}), fill_mode: 'custom', custom_fill: '#435468' },
})),
})));
await page.evaluate(async () => {
const card = window.__card;
card.hass = { ...card.hass, states: { ...card.hass.states, 'sun.sun': {
entity_id: 'sun.sun', state: 'above_horizon',
attributes: { azimuth: 180, elevation: 40, rising: false },
} } };
await card.updateComplete;
});
await settle();
};
const stageBox = () => page.evaluate(() => {
const rect = window.__card.renderRoot.querySelector('.stage').getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const snapshot = async () => {
await page.waitForTimeout(80);
const state = await page.evaluate(() => {
const root = window.__card.renderRoot;
const stage = root.querySelector('.stage');
const plan = root.querySelector('.plan-svg');
const outline = root.querySelector('.hp-paper-outline-svg');
return {
zoom: window.__card._zoom,
daycycle: stage.classList.contains('daycycle'),
safe: stage.classList.contains('hp-safe-daycycle-outline'),
planWillChange: getComputedStyle(plan).willChange,
planTransform: getComputedStyle(plan).transform,
outlineOverflow: outline ? getComputedStyle(outline).overflow : null,
outlineInline: outline ? `${outline.style.overflow}|${outline.style.clipPath}` : null,
sceneClips: [...root.querySelectorAll('[data-hp-live-viewbox]')].map((node) => node.style.clipPath || ''),
};
});
const layers = [];
for (const layer of latestLayers.filter((item) => item.drawsContent)) {
let name = '';
if (layer.backendNodeId) {
try {
const { node } = await cdp.send('DOM.describeNode', { backendNodeId: layer.backendNodeId });
const attrs = node.attributes || [];
const index = attrs.indexOf('class');
name = index >= 0 ? attrs[index + 1] : node.nodeName;
} catch { /* detached between the event and inspection */ }
}
let reasons = [];
try {
({ compositingReasons: reasons } = await cdp.send('LayerTree.compositingReasons', {
layerId: layer.layerId,
}));
} catch { /* retired layers may have no reason record */ }
layers.push({ name, width: layer.width, height: layer.height, reasons });
}
return { state, layers };
};
// Everything the card paints; the page root and the sticky header are not plan.
const planLayers = (snap) => snap.layers.filter((layer) => layer.name
&& layer.name !== '#document' && !/\bhdr\b/.test(layer.name));
const within = (snap, stage, factor) => planLayers(snap).every((layer) =>
layer.width <= stage.width * factor + 240 && layer.height <= stage.height * factor + 240);
const largest = (snap) => planLayers(snap).reduce((max, layer) =>
Math.max(max, layer.width * layer.height), 0);
const wheelAt = async (stage, deltaY, count) => {
// Near a corner: the hover tooltip stays out of the central frame sample.
await page.mouse.move(stage.x + 40, stage.y + 40);
for (let i = 0; i < count; i++) {
await page.mouse.wheel(0, deltaY);
await page.waitForTimeout(24);
}
};
await dayNight();
const stage = await stageBox();
await cdp.send('Page.startScreencast', { format: 'png', everyNthFrame: 2, maxWidth: 820, maxHeight: 760 });
// A: page loaded at 100 %, zoomed to 800 % with the wheel (owner's blur path).
await wheelAt(stage, -240, 30);
const zoomingIn = await snapshot();
await settle();
const zoomedIn = await snapshot();
// The buttons and a pinch settle into the same hint-free scene (AC1).
const clickZoom = (hook) => page.evaluate((hook) =>
window.__card.renderRoot.querySelector(`[data-hp="${hook}"]`).click(), hook);
await clickZoom('zoom-out'); await settle();
await clickZoom('zoom-in'); await settle();
const buttons = await snapshot();
await page.evaluate(() => {
const stage = window.__card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
stage.setPointerCapture = () => {};
stage.releasePointerCapture = () => {};
const emit = (type, id, x) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id, pointerType: 'touch',
isPrimary: id === 6891, button: type === 'pointerdown' ? 0 : -1,
buttons: type === 'pointerup' ? 0 : 1, clientX: x, clientY: rect.top + rect.height / 2,
}));
const cx = rect.left + rect.width / 2;
emit('pointerdown', 6891, cx - 120); emit('pointerdown', 6892, cx + 120);
emit('pointermove', 6891, cx - 90); emit('pointermove', 6892, cx + 90);
emit('pointerup', 6891, cx - 90); emit('pointerup', 6892, cx + 90);
});
await settle();
const pinched = await snapshot();
await wheelAt(stage, -240, 12);
await settle();
// B: reload at 800 % (the camera is restored), day/night again, then navigate
// (owner's white-flash path).
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__card?._model?.length > 0, { timeout: 9000 });
await installHpTestOnPage(page);
await dayNight();
const reloaded = await snapshot();
await wheelAt(stage, 240, 3);
const navigating = await snapshot();
await wheelAt(stage, -240, 6);
await settle();
const navigated = await snapshot();
await cdp.send('Page.stopScreencast');
const nearWhite = await page.evaluate(async ({ encoded, stage }) => {
const out = [];
for (const data of encoded) {
const image = await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:image/png;base64,${data}`;
});
const sx = image.width / innerWidth, sy = image.height / innerHeight;
const w = Math.max(1, Math.floor(stage.width * sx)), h = Math.max(1, Math.floor(stage.height * sy));
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(image, Math.floor(stage.x * sx), Math.floor(stage.y * sy), w, h, 0, 0, w, h);
const px = context.getImageData(0, 0, w, h).data;
let sampled = 0, white = 0;
for (let y = Math.floor(h * 0.25); y < h * 0.75; y += 3) {
for (let x = Math.floor(w * 0.25); x < w * 0.75; x += 3) {
const o = (y * w + x) * 4;
sampled++;
if (px[o] >= 250 && px[o + 1] >= 250 && px[o + 2] >= 250) white++;
}
}
out.push(white / Math.max(1, sampled));
}
return out;
}, { encoded: frames, stage });
const planReasons = (snap) => planLayers(snap).find((layer) => /\bplan-svg\b/.test(layer.name))?.reasons || [];
const out = {
dayNightSafeAt800: zoomedIn.state.daycycle && zoomedIn.state.safe && zoomedIn.state.zoom === 8,
reloadRestores800Untouched: reloaded.state.daycycle && !reloaded.state.safe && reloaded.state.zoom === 8,
// K1: no frozen raster — the explicit layer is a trivial 3D transform.
settledSceneHasNoWillChangeHint: [zoomedIn, buttons, pinched, navigated]
.every((snap) => snap.state.planWillChange === 'opacity'),
buttonAndPinchChangedTheCamera: buttons.state.safe && pinched.state.zoom < 8,
// The scene stays an explicit layer (#582), but no reason is the hint that
// freezes the raster scale, and it is never an implicit overlap layer.
settledSceneLayerIsExplicitWithoutHint: planReasons(zoomedIn).some((reason) => /will-change: opacity/.test(reason))
&& planReasons(zoomedIn).every((reason) => !/will-change: transform/.test(reason)
&& !/Overlaps other composited content/.test(reason)),
// K3: layers stay stage-sized at 800 %.
gestureLayersWithinBudget: within(zoomingIn, stage, 1.5) && within(navigating, stage, 1.5),
settledLayersWithinBudget: [zoomedIn, buttons, pinched, reloaded, navigated]
.every((snap) => within(snap, stage, 1)),
outlineClippedToItsBox: zoomedIn.state.outlineOverflow === 'hidden'
&& navigating.state.outlineInline === '|' && navigated.state.outlineInline === '|',
idleScenesKeepNoClip: [zoomedIn, buttons, pinched, reloaded, navigated].every((snap) => snap.state.sceneClips.every((clip) => clip === '')),
capturedFrames: nearWhite.length >= 3,
// A white/transparent frame covers the stage; white glyphs of 800 % device
// markers stay a few per cent of the central sample.
noWhiteFrames: nearWhite.every((ratio) => ratio < 0.25),
};
console.log(JSON.stringify({
stage,
largest: {
zoomingIn: largest(zoomingIn), zoomedIn: largest(zoomedIn), reloaded: largest(reloaded),
navigating: largest(navigating), navigated: largest(navigated),
},
layers: { zoomingIn: planLayers(zoomingIn), zoomedIn: planLayers(zoomedIn), navigating: planLayers(navigating) },
states: {
zoomingIn: zoomingIn.state, zoomedIn: zoomedIn.state, buttons: buttons.state,
pinched: pinched.state, reloaded: reloaded.state, navigated: navigated.state,
},
whiteMax: Math.max(0, ...nearWhite),
}, null, 1));
checkAll(out);
await cdp.send('LayerTree.disable');
await finish(browser, out);
+1 -1
View File
@@ -225,7 +225,7 @@ const res = await page.evaluate(async () => {
await popoverPicker?.updateComplete;
// seven tools (the six drawing ones + «Мебель», docs/FURNITURE.md) plus
// «Картинка-подложка», which f1 offers because it HAS a picture
// (docs/BACKDROP.md §2); a hand-drawn space still shows seven
// (docs/DECOR-EDITOR.md §3.2); a hand-drawn space still shows seven
out.toolBtns = sr().querySelectorAll('.decorbar .btn.dtool').length === 8;
// 2) нарисовать прямоугольник drag-ом (через прямые вызовы)
c._decorTool = 'rect'; c._decorStyle = { color: '#ff0000', width: 3, fill: true }; await c.updateComplete;
+2 -2
View File
@@ -89,7 +89,7 @@ const res = await page.evaluate(async () => {
const y0 = tsp[0] ? +tsp[0].getAttribute('y') : NaN;
const y1 = tsp[1] ? +tsp[1].getAttribute('y') : NaN;
out.blockCentredVertically = Math.abs((y0 + y1) / 2 - ay) < 0.01 && y1 > y0;
// длинная строка НЕ переносится сама (docs/LIVE-TEXT.md: никаких автопереносов)
// длинная строка НЕ переносится сама (docs/DECOR-EDITOR.md §5: никаких автопереносов)
open(multi); await c.updateComplete;
c._decorTextDialog = { ...c._decorTextDialog, text: 'x'.repeat(120) };
c._decorSaveText(); await c.updateComplete;
@@ -109,7 +109,7 @@ const res = await page.evaluate(async () => {
&& frame().querySelectorAll('.dthandle').length === 5
&& !!frame().querySelector('.dtrot');
// --- визуал в 4 раза меньше, хит-зона прежняя (docs/LIVE-TEXT.md §3) ---
// --- визуал в 4 раза меньше, хит-зона прежняя (docs/DECOR-EDITOR.md §5.3) ---
out.fiveVisibleKnobs = frame()?.querySelectorAll('.dtknob').length === 5;
const rOf = (sel) => { const e = frame()?.querySelector(sel); return e ? +e.getAttribute('r') : NaN; };
const hitR = rOf('.dthandle.dtrot');
+2 -1
View File
@@ -14,7 +14,8 @@ const res = await page.evaluate(async () => {
out.gearReadable = cs ? parseFloat(cs.fontSize) > 0 && cs.pointerEvents === 'auto' : null;
out.gearHasLabel = btn ? btn.textContent.trim().length > 0 : null;
const box = btn?.getBoundingClientRect();
// (в редакторе плана .dev скрыты display:none — сравнивать не с чем)
// (маркеры устройств в редакторе плана — неинтерактивные ориентиры #687,
// кнопку с ними не сравниваем)
out.gearTapTarget = box ? box.height > 6 && box.height < 40 : null;
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
await c.updateComplete;
+28 -9
View File
@@ -26,24 +26,43 @@ const res = await page.evaluate(async () => {
// 2) редакторы заблокированы
c._setMode('plan'); await c.updateComplete;
out.editorsBlocked = c._mode === 'view';
// 3) свайп при 1:1 переключает пространство (и точки показываются)
// 3) центральный drag остаётся pan, а свайп из внутренней 48 px зоны
// правого края при 1:1 переключает на соседнее пространство.
const s0 = c._space;
const stage = sr().querySelector('.stage');
stage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, composed: true, pointerId: 1, pointerType: 'touch', isPrimary: true, button: 0, clientX: 600, clientY: 300 }));
stage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, composed: true, pointerId: 1, pointerType: 'touch', isPrimary: true, button: 0, clientX: 450, clientY: 305 }));
const fire = (type, pointerId, clientX, clientY) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId, pointerType: 'touch',
isPrimary: true, button: 0, buttons: type === 'pointerup' ? 0 : 1, clientX, clientY,
}));
const rect = stage.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const centerView = JSON.stringify(c._viewOr(c._baseVb()));
fire('pointerdown', 1, centerX, centerY);
fire('pointermove', 1, centerX - 150, centerY + 5);
fire('pointerup', 1, centerX - 150, centerY + 5);
await c.updateComplete;
out.swipeSwitches = c._space !== s0;
out.centralDragPansWithoutSwitching = c._space === s0
&& JSON.stringify(c._viewOr(c._baseVb())) !== centerView && c._panLock === null;
const rightEdgeX = rect.right - 20;
fire('pointerdown', 2, rightEdgeX, centerY);
fire('pointermove', 2, rightEdgeX - 80, centerY + 2);
fire('pointerup', 2, rightEdgeX - 120, centerY + 3);
await c.updateComplete;
out.edgeSwipeSwitches = c._space !== s0;
out.dotsShown = !!sr().querySelector('.kioskdots');
// 4) при зуме свайп не переключает
c._zoom = 2; const s1 = c._space;
stage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, composed: true, pointerId: 2, pointerType: 'touch', isPrimary: true, button: 0, clientX: 600, clientY: 300 }));
stage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, composed: true, pointerId: 2, pointerType: 'touch', isPrimary: true, button: 0, clientX: 450, clientY: 305 }));
const leftEdgeX = rect.left + 20;
fire('pointerdown', 3, leftEdgeX, centerY);
fire('pointermove', 3, leftEdgeX + 80, centerY + 2);
fire('pointerup', 3, leftEdgeX + 120, centerY + 3);
await c.updateComplete;
out.noSwipeZoomed = c._space === s1;
// 5) двойной тап сбрасывает зум
for (const pointerId of [3, 4]) {
stage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, composed: true, pointerId, pointerType: 'touch', isPrimary: true, button: 0, clientX: 500, clientY: 300 }));
stage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, composed: true, pointerId, pointerType: 'touch', isPrimary: true, button: 0, clientX: 501, clientY: 300 }));
for (const pointerId of [4, 5]) {
fire('pointerdown', pointerId, centerX, centerY);
fire('pointerup', pointerId, centerX + 1, centerY);
}
const resetStarted = performance.now();
while (c._cameraTransition?.active) {
+57 -16
View File
@@ -2,8 +2,9 @@
// audit DEV-1DA1-02 (P2).
//
// `_stagePointerMove` locks the gesture on the first movement past 8 px
// (`_panLock`: 'swipe' if it is horizontal enough inside the swipe zone, 'pan'
// otherwise) and the plan then follows the finger. But `_stagePointerUp` used
// (`_panLock`: 'swipe' only for a sufficiently horizontal inward move that
// started inside the 48 px strip of an edge with a neighbour, 'pan' otherwise).
// A pan then follows the finger. But `_stagePointerUp` used
// to ignore that lock and ask `swipeTarget()` again, from the raw start→end
// vector alone. A CURVED gesture — a small vertical lead-in that locks 'pan',
// then a long horizontal sweep — therefore panned under the finger and still
@@ -12,7 +13,7 @@
//
// The lock is now final: with `_panLock === 'pan'` the floor never changes, no
// matter what the overall vector looks like. The two straight gestures keep
// their old behaviour, and so does the motionless double tap (no movement,
// their intended behaviour, and so does the motionless double tap (no movement,
// no lock — the swipe path is never even reached by it).
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
@@ -29,6 +30,10 @@ const out = await page.evaluate(async () => {
await k.updateComplete;
const sr = k.shadowRoot || k.renderRoot;
const stage = sr.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const rightEdgeX = rect.right - 20;
const fire = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id,
pointerType: 'touch', isPrimary: true, button: 0, clientX: x, clientY: y,
@@ -36,8 +41,10 @@ const out = await page.evaluate(async () => {
o.kioskHasSeveralSpaces = k._model.length > 1;
const home = async () => {
k._commitSpace(k._model[0].id, true);
const vb = k._baseVb();
k._applyView(1, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
k.requestUpdate();
await k.updateComplete;
};
/**
@@ -67,42 +74,76 @@ const out = await page.evaluate(async () => {
// ---- 1. the auditor's curved pan: vertical lead-in, horizontal ending --
// (500,300) → (502,312) locks 'pan' → curve left to (350,304) → release.
// dx = -150, dy = +4: swipeTarget() would happily call that a swipe.
const curvedPan = await play(51, [500, 300], [[502, 312], [460, 310], [400, 306], [350, 304]]);
const curvedPan = await play(51, [centerX, centerY], [
[centerX + 2, centerY + 12], [centerX - 40, centerY + 10],
[centerX - 100, centerY + 6], [centerX - 150, centerY + 4],
]);
o.curvedPanLocksPan = curvedPan.lockAfterLeadIn === 'pan';
o.curvedPanKeepsTheLock = curvedPan.lockBeforeRelease === 'pan';
o.curvedPanActuallyPans = curvedPan.moved === true;
o.curvedPanKeepsTheFloor = curvedPan.s1 === curvedPan.s0;
// the same trajectory the other way round — a swipe to the right would have
// been the previous floor, so the bug is symmetric and so is the fix
const curvedPanRight = await play(52, [400, 300], [[402, 312], [460, 308], [520, 305], [560, 304]]);
const curvedPanRight = await play(52, [centerX, centerY], [
[centerX + 2, centerY + 12], [centerX + 60, centerY + 8],
[centerX + 120, centerY + 5], [centerX + 160, centerY + 4],
]);
o.curvedPanRightLocksPan = curvedPanRight.lockAfterLeadIn === 'pan';
o.curvedPanRightKeepsTheFloor = curvedPanRight.s1 === curvedPanRight.s0;
// a long diagonal that ends up dominated by x, but started as a pan
const diagonalPan = await play(53, [500, 300], [[496, 316], [420, 340], [330, 350], [260, 352]]);
const diagonalPan = await play(53, [centerX, centerY], [
[centerX - 4, centerY + 16], [centerX - 80, centerY + 40],
[centerX - 170, centerY + 50], [centerX - 240, centerY + 52],
]);
o.diagonalPanLocksPan = diagonalPan.lockAfterLeadIn === 'pan';
o.diagonalPanKeepsTheFloor = diagonalPan.s1 === diagonalPan.s0;
// Even when a drag starts in the active edge strip, a vertical first move
// owns it as pan forever; a later horizontal tail cannot reclassify release.
const edgeCurvedPan = await play(62, [rightEdgeX, centerY], [
[rightEdgeX - 2, centerY + 14], [rightEdgeX - 80, centerY + 10],
[rightEdgeX - 170, centerY + 5],
]);
o.edgeCurvedPanLocksPan = edgeCurvedPan.lockAfterLeadIn === 'pan';
o.edgeCurvedPanActuallyPans = edgeCurvedPan.moved === true;
o.edgeCurvedPanKeepsTheFloor = edgeCurvedPan.s1 === edgeCurvedPan.s0;
// ---- 2. a gesture locked as a SWIPE keeps its own semantics ------------
// horizontal lead-in locks 'swipe'; the plan must not slide under it, even
// when the trajectory then bends vertically and the final vector no longer
// qualifies — the floor simply stays, and nothing pans
const curvedSwipe = await play(54, [600, 300], [[540, 302], [536, 380], [545, 500]]);
const curvedSwipe = await play(54, [rightEdgeX, centerY], [
[rightEdgeX - 60, centerY + 2], [rightEdgeX - 64, centerY + 80],
[rightEdgeX - 55, centerY + 200],
]);
o.curvedSwipeLocksSwipe = curvedSwipe.lockAfterLeadIn === 'swipe';
o.curvedSwipeNeverPans = curvedSwipe.moved === false;
o.curvedSwipeThatDiesChangesNothing = curvedSwipe.s1 === curvedSwipe.s0;
// a swipe that bends but still ends as a swipe does switch the floor
const bentSwipe = await play(55, [600, 300], [[540, 302], [470, 330], [420, 340]]);
const bentSwipe = await play(55, [rightEdgeX, centerY], [
[rightEdgeX - 60, centerY + 2], [rightEdgeX - 130, centerY + 30],
[rightEdgeX - 180, centerY + 40],
]);
o.bentSwipeLocksSwipe = bentSwipe.lockAfterLeadIn === 'swipe';
o.bentSwipeNeverPans = bentSwipe.moved === false;
o.bentSwipeStillSwitches = bentSwipe.s1 !== bentSwipe.s0;
// ---- 3. the straight gestures are exactly as they were -----------------
const straightSwipe = await play(56, [600, 300], [[540, 302], [480, 305], [450, 305]]);
const straightSwipe = await play(56, [rightEdgeX, centerY], [
[rightEdgeX - 60, centerY + 2], [rightEdgeX - 120, centerY + 5],
[rightEdgeX - 150, centerY + 5],
]);
o.straightSwipeSwitches = straightSwipe.s1 !== straightSwipe.s0;
o.straightSwipeDoesNotPan = straightSwipe.moved === false;
const straightPan = await play(57, [450, 200], [[452, 260], [454, 330]]);
const straightPan = await play(57, [centerX, centerY - 100], [
[centerX + 2, centerY - 40], [centerX + 4, centerY + 30],
]);
o.straightPanPans = straightPan.moved === true;
o.straightPanKeepsTheFloor = straightPan.s1 === straightPan.s0;
const centralHorizontalPan = await play(61, [centerX, centerY], [
[centerX - 60, centerY + 2], [centerX - 150, centerY + 5],
]);
o.centralHorizontalDragPans = centralHorizontalPan.moved === true;
o.centralHorizontalDragKeepsTheFloor = centralHorizontalPan.s1 === centralHorizontalPan.s0;
// ---- 4. a motionless double tap still resets the zoom ------------------
// no movement means no lock at all, so nothing above can reach this path
@@ -110,8 +151,8 @@ const out = await page.evaluate(async () => {
k._applyView(2);
await k.updateComplete;
for (const id of [58, 60]) {
fire('pointerdown', id, 500, 300);
fire('pointerup', id, 501, 300);
fire('pointerdown', id, centerX, centerY);
fire('pointerup', id, centerX + 1, centerY);
}
const resetStarted = performance.now();
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
@@ -125,11 +166,11 @@ const out = await page.evaluate(async () => {
await k.updateComplete;
const zoomed = await (async () => {
const s0 = k._space;
fire('pointerdown', 59, 600, 300);
fire('pointermove', 59, 540, 302);
fire('pointerdown', 59, rightEdgeX, centerY);
fire('pointermove', 59, rightEdgeX - 60, centerY + 2);
const lock = k._panLock;
fire('pointermove', 59, 480, 305);
fire('pointerup', 59, 480, 305);
fire('pointermove', 59, rightEdgeX - 120, centerY + 5);
fire('pointerup', 59, rightEdgeX - 120, centerY + 5);
await k.updateComplete;
return { lock, same: k._space === s0 };
})();
+9 -2
View File
@@ -52,12 +52,19 @@ const liveState = () => page.evaluate(() => {
view: structuredClone(card._view),
transformed: scenes.filter((node) => node.style.transform).length,
sceneCount: scenes.length,
// #689: an exposed scene opens only within its bound; the filtered
// day-cycle outline (marked clip) is projected but never opened.
allSceneOverflowOpen: scenes.length > 0
&& scenes.every((node) => node.style.overflow === 'visible'),
&& scenes.every((node) => (node.getAttribute('data-hp-live-overflow') === 'clip'
? !node.style.overflow && !node.style.clipPath
: node.style.overflow === 'visible' && node.style.clipPath === 'inset(-25%)')),
allScenePromoted: scenes.length > 0 && scenes.every((node) => node.style.transform
&& node.style.transformOrigin && node.style.willChange === 'transform'
&& node.style.overflow === 'visible'),
&& (node.getAttribute('data-hp-live-overflow') === 'clip'
? !node.style.overflow && !node.style.clipPath
: node.style.overflow === 'visible' && node.style.clipPath === 'inset(-25%)')),
allTemporaryStylesCleared: scenes.every((node) => !node.style.overflow
&& !node.style.clipPath
&& !node.style.transform && !node.style.transformOrigin && !node.style.willChange),
marker: centre(root.querySelector('[data-hp="device"]')),
room: centre(root.querySelector('[data-hp="room"]')),
+1 -1
View File
@@ -1,4 +1,4 @@
// Живой текст в декоре (docs/LIVE-TEXT.md): все связи с HA живут прямо в
// Живой текст в декоре (docs/DECOR-EDITOR.md §5): все связи с HA живут прямо в
// тексте как {entity} / {entity:attribute}. Один блок может смешивать обычный
// текст и несколько переменных; выбор значения вставляет токен в позицию
// курсора. Старые entity/attr/unit читаются, но первое сохранение мигрирует их.
+3 -2
View File
@@ -48,10 +48,11 @@ out.viewHoldInfo = await page.evaluate(() => { const r = !!window.__card._infoCa
await page.evaluate(() => window.__card._setMode('plan'));
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
out.plan = await st();
out.planIconsHidden = await page.evaluate(() => {
// #687: Plan keeps device markers as translucent, inert landmarks.
out.planIconsVisible = await page.evaluate(() => {
const sr = window.__card.shadowRoot || window.__card.renderRoot;
const dev = sr.querySelector('.dev');
return dev ? getComputedStyle(dev).display === 'none' : 'no-dev';
return dev ? getComputedStyle(dev).display !== 'none' : false;
});
// 4) режим Устройства: drag работает, клик открывает редактор
await page.evaluate(() => window.__card._setMode('devices'));
+16 -10
View File
@@ -9,7 +9,8 @@
// at 800 %, in view mode and in every editor; how far you may walk is decided
// by `_clampView` (PAN_SLACK: about a screen past the content) and not by the
// zoom. The tools keep the pointer they already owned — a resize handle, a
// device badge, an opening — and the kiosk keeps its floor swipe.
// device badge, an opening — and the kiosk keeps its floor swipe from the
// active 48 px edge while central horizontal drags remain pan.
import { launch, check, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const out = {};
@@ -192,7 +193,7 @@ Object.assign(out, await page.evaluate(async () => {
return o;
}));
// ---- the kiosk keeps its floor swipe ---------------------------------------
// ---- the kiosk keeps its edge-owned floor swipe ----------------------------
Object.assign(out, await page.evaluate(async () => {
const c0 = window.__card;
const o = {};
@@ -207,19 +208,24 @@ Object.assign(out, await page.evaluate(async () => {
const sr = k.shadowRoot || k.renderRoot;
const stage = sr.querySelector('.stage');
const fire = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id, clientX: x, clientY: y,
bubbles: true, composed: true, cancelable: true, pointerId: id,
pointerType: 'touch', isPrimary: true, button: 0,
buttons: type === 'pointerup' ? 0 : 1, clientX: x, clientY: y,
}));
k._applyView(1); await k.updateComplete;
const rect = stage.getBoundingClientRect();
const rightEdgeX = rect.right - 20;
const centerY = rect.top + rect.height / 2;
const s0 = k._space;
const v0 = { ...k._viewOr(k._baseVb()) };
// a REAL swipe: down, several moves, up — the moves are what used to be
// impossible to add to smoke_kiosk without hijacking the gesture
fire('pointerdown', 41, 600, 300);
fire('pointermove', 41, 540, 302);
fire('pointermove', 41, 480, 305);
fire('pointermove', 41, 450, 305);
// A real floor swipe starts inside the active right-edge strip and moves
// inward. Several moves prove that the plan never pans under the gesture.
fire('pointerdown', 41, rightEdgeX, centerY);
fire('pointermove', 41, rightEdgeX - 60, centerY + 2);
fire('pointermove', 41, rightEdgeX - 120, centerY + 5);
fire('pointermove', 41, rightEdgeX - 150, centerY + 5);
const midPan = Math.abs(k._viewOr(k._baseVb()).x - v0.x);
fire('pointerup', 41, 450, 305);
fire('pointerup', 41, rightEdgeX - 150, centerY + 5);
await k.updateComplete;
o.kioskSwipeStillSwitchesFloors = k._space !== s0;
o.kioskHorizontalDragIsNotAPan = midPan < 1;
+206
View File
@@ -0,0 +1,206 @@
// #687: in the Plan editor device markers are landmarks exactly as in the
// Background editor (#362) — the same markers as View, at the Background
// editor's effective opacity, and fully pointer-inert so every Plan tool
// receives the point below them. Room labels (same .devlayer) stay opaque.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const probe = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const settleMode = async () => {
const started = performance.now();
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
while (c._modeTransitionBusy && performance.now() - started < 1500);
await c.updateComplete;
};
// Entering a mode settles the camera after the stage resizes; wait until
// marker positions hold still so measured points are the clicked points.
const layoutKey = () => [...sr().querySelectorAll('.devlayer [data-hp="device"]')]
.map((node) => { const r = node.getBoundingClientRect(); return `${r.left.toFixed(1)},${r.top.toFixed(1)}`; })
.join('|');
const enter = async (mode) => {
if (c._mode !== mode) await window.__hpTest.setMode(mode);
await settleMode();
const started = performance.now();
let key = layoutKey(), still = 0;
while (still < 10 && performance.now() - started < 3000) {
await new Promise((resolve) => requestAnimationFrame(resolve));
const next = layoutKey();
still = next === key ? still + 1 : 0;
key = next;
}
await c.updateComplete;
};
const stage = () => sr().querySelector('.stage');
const markers = () => [...sr().querySelectorAll('.devlayer [data-hp="device"]')];
const visible = (node) => {
const r = node.getBoundingClientRect();
return getComputedStyle(node).display !== 'none' && r.width > 0 && r.height > 0;
};
const visibleIds = () => markers().filter(visible).map((node) => node.dataset.id).sort();
// Effective alpha of one marker: its own and every ancestor's opacity and
// filter: opacity(), up to and including the stage.
const effectiveOpacity = (node) => {
let alpha = 1;
for (let el = node; el; el = el.parentElement) {
const cs = getComputedStyle(el);
alpha *= Number(cs.opacity);
for (const m of cs.filter.matchAll(/opacity\(([^)]+)\)/g)) alpha *= Number(m[1]);
if (el === stage()) break;
}
return alpha;
};
const opacityById = () => Object.fromEntries(markers().filter(visible)
.map((node) => [node.dataset.id, effectiveOpacity(node)]));
// One unavailable marker exercises contract item 2: the fade multiplies
// the marker's own opacity (.unavail) exactly as the Background layer does.
const target = markers().find((node) => node.dataset.entity && c.hass.states[node.dataset.entity]);
if (target) {
const entity = target.dataset.entity;
c.hass = { ...c.hass, states: { ...c.hass.states,
[entity]: { ...c.hass.states[entity], state: 'unavailable' } } };
await c.updateComplete;
}
await enter('view');
out.unavailableMarkerInView = !!sr().querySelector('.devlayer [data-hp="device"].unavail');
const viewIds = visibleIds();
out.viewHasMarkers = viewIds.length > 0;
await enter('decor');
const decorOpacity = opacityById();
await enter('plan');
const planIds = visibleIds();
const planOpacity = opacityById();
// AC1 — the same markers as View, each at the Background editor's alpha.
out.planShowsViewMarkers = JSON.stringify(planIds) === JSON.stringify(viewIds);
out.planOpacityMatchesBackground = planIds.length > 0 && planIds.every((id) =>
id in decorOpacity && Math.abs(planOpacity[id] - decorOpacity[id]) < 1e-3);
const plain = markers().find((node) => visible(node) && !node.classList.contains('unavail'));
out.planPlainMarkerAt35 = !!plain && Math.abs(planOpacity[plain.dataset.id] - 0.35) < 1e-3;
const unavail = markers().find((node) => visible(node) && node.classList.contains('unavail'));
out.planUnavailableMarkerAt35x35 = !!unavail
&& Math.abs(planOpacity[unavail.dataset.id] - 0.35 * 0.35) < 1e-3;
// Room labels share .devlayer: no layer fade reaches them (their own
// opacity is the label design, not this contract).
const label = sr().querySelector('.roomlabel');
out.roomLabelNotFaded = !!label
&& Math.abs(effectiveOpacity(label) - Number(getComputedStyle(label).opacity)) < 1e-3
&& !/opacity\(/.test(getComputedStyle(label).filter);
out.planMarkersOutOfTabOrder = markers().every((node) => !node.hasAttribute('tabindex')
&& !node.hasAttribute('role'));
// AC2 — pick a marker wholly inside the stage, preferring a value capsule
// that extends outside the marker's nominal box.
const s = stage().getBoundingClientRect();
const inside = markers().filter((node) => {
const r = node.getBoundingClientRect();
return visible(node) && r.left > s.left + 4 && r.right < s.right - 4
&& r.top > s.top + 4 && r.bottom < s.bottom - 4;
});
const at = (p) => p ? sr().elementFromPoint(p.x, p.y) : null;
const centre = (node) => {
const b = node.getBoundingClientRect();
return { x: b.left + b.width / 2, y: b.top + b.height / 2 };
};
// Room chrome lying under a marker keeps the hit (the demo plan puts a
// marker over a room's settings button).
const roomChrome = '.roomlabel, [data-hp="room-settings"]';
const overLabel = inside.find((node) => at(centre(node))?.closest?.(roomChrome));
out.roomChromeUnderMarkerGetsHit = !!overLabel;
const gearPoint = overLabel && at(centre(overLabel))?.closest?.('[data-hp="room-settings"]')
? centre(overLabel) : null;
// The tool probe lies over bare plan, so the Walls tool owns the point;
// prefer a value capsule that extends outside the marker's nominal box.
const overPlan = inside.filter((node) => !at(centre(node))?.closest?.('.devlayer'));
const dev = overPlan.find((node) => node.classList.contains('valonly')
|| node.querySelector('.device-shell.with-values')) || overPlan[0];
const frame = dev?.querySelector('.device-shell-frame');
const r = dev?.getBoundingClientRect();
const fr = frame?.getBoundingClientRect();
const core = dev ? centre(dev) : null;
const capsule = fr && fr.right > r.right + 2
? { x: fr.right - 1, y: fr.top + fr.height / 2 } : core;
out.probeFound = !!dev && !!core;
out.probeHasOutsideCapsule = !!fr && fr.right > r.right + 2;
const cursorAt = (p) => { const t = at(p); return t ? getComputedStyle(t).cursor : null; };
const hit = { core: at(core), capsule: at(capsule) };
const cursor = { core: cursorAt(core), capsule: cursorAt(capsule) };
out.coreFallsThrough = !!hit.core && !hit.core.closest('.dev');
out.capsuleFallsThrough = !!hit.capsule && !hit.capsule.closest('.dev');
out.markerSubtreeIsPointerInert = !!dev && !!frame
&& getComputedStyle(dev).pointerEvents === 'none'
&& getComputedStyle(dev, '::before').pointerEvents === 'none'
&& getComputedStyle(frame).pointerEvents === 'none'
&& [...dev.querySelectorAll('*')].every((el) => getComputedStyle(el).pointerEvents === 'none');
// "The cursor over a marker is the cursor of what lies below": the same
// point with the marker taken out of layout gives the same target/cursor.
if (dev) {
dev.style.display = 'none';
out.targetSameWithoutMarker = at(core) === hit.core && at(capsule) === hit.capsule;
out.cursorSameWithoutMarker = cursorAt(core) === cursor.core && cursorAt(capsule) === cursor.capsule;
dev.style.display = '';
}
await window.__hpTest.setTool('draw');
out.wallsChainEmptyBeforeClick = c._path.length === 0;
return { out, core, id: dev?.dataset.id, gearPoint };
});
// A real mouse click on the marker core reaches the Walls tool. It runs
// before the synthetic probes below, which leave pointer state behind.
if (probe.core) await page.mouse.click(probe.core.x, probe.core.y);
// A real click on a marker lying over a room's settings button opens the
// room dialog: the button below owns the point.
if (probe.gearPoint) await page.mouse.click(probe.gearPoint.x, probe.gearPoint.y);
const gear = await page.evaluate(async () => {
const c = window.__card;
await c.updateComplete;
const opened = c._roomDialog === true && !!c._roomEditId;
if (opened) await window.__hpTest.close(undefined, { via: 'cancel' });
return { roomSettingsOpenThroughMarker: opened, roomDialogClosed: !c._roomDialog };
});
const inert = await page.evaluate(async ({ core, id }) => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
await c.updateComplete;
out.wallsToolReceivesClickThroughMarker = c._path.length === 1;
const dev = [...sr().querySelectorAll('.devlayer [data-hp="device"]')].find((node) => node.dataset.id === id);
const r = dev?.getBoundingClientRect();
// The click above must have landed on the probed marker.
out.probeDidNotMove = !!r && !!core
&& Math.abs(r.left + r.width / 2 - core.x) < 1 && Math.abs(r.top + r.height / 2 - core.y) < 1;
// Handlers fail closed even when events are dispatched on the marker itself.
let serviceCalls = 0, wsCalls = 0;
const oldCallService = c.hass.callService;
const oldCallWS = c.hass.callWS;
c.hass.callService = () => { serviceCalls += 1; };
c.hass.callWS = async () => { wsCalls += 1; return { ok: true }; };
const before = { tip: c._tip, info: c._infoCard, drag: c._deviceDrag, sel: c._selId };
const opts = { bubbles: true, composed: true, cancelable: true, clientX: core?.x || 0, clientY: core?.y || 0 };
dev?.dispatchEvent(new PointerEvent('pointerover', { ...opts, pointerId: 687, pointerType: 'mouse', isPrimary: true }));
dev?.dispatchEvent(new PointerEvent('pointermove', { ...opts, pointerId: 687, pointerType: 'mouse', isPrimary: true }));
dev?.dispatchEvent(new PointerEvent('pointerdown', { ...opts, pointerId: 688, pointerType: 'mouse', button: 0, isPrimary: true }));
dev?.dispatchEvent(new PointerEvent('pointerup', { ...opts, pointerId: 688, pointerType: 'mouse', button: 0, isPrimary: true }));
dev?.dispatchEvent(new MouseEvent('click', { ...opts, button: 0 }));
const ctx = new MouseEvent('contextmenu', { ...opts, button: 2 });
dev?.dispatchEvent(ctx);
await c.updateComplete;
out.handlersFailClosed = serviceCalls === 0 && wsCalls === 0
&& c._tip === before.tip && c._infoCard === before.info
&& c._deviceDrag === before.drag && c._selId === before.sel;
out.noDeviceHoverAttribute = !sr().querySelector('.dev[data-hp-device-hover]');
out.contextMenuNotClaimed = !ctx.defaultPrevented;
c.hass.callService = oldCallService;
c.hass.callWS = oldCallWS;
return out;
}, { core: probe.core, id: probe.id });
const res = { ...probe.out, ...gear, ...inert };
checkAll(res);
await finish(browser, res);
+56 -13
View File
@@ -45,6 +45,8 @@ const out = await page.evaluate(async () => {
}
await c.updateComplete;
};
const waitMs = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const waitRoomDecision = () => waitMs(370);
const pointer = (target, type, id, x = 500, y = 300, extra = {}) =>
target.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id,
@@ -66,8 +68,17 @@ const out = await page.evaluate(async () => {
};
localStorage.setItem('houseplan_card_zoom_v1', JSON.stringify({ 'fit-floor': 1 }));
const beforeDelayedRoomFit = JSON.stringify({ zoom: c._zoom, view: c._view });
pointer(roomNode(), 'pointerdown', 15201);
pointer(roomNode(), 'pointerup', 15201);
result.singleRoomTapWaitsForSecondTap = !c._cameraTransition?.active
&& c._roomFocus === null
&& JSON.stringify({ zoom: c._zoom, view: c._view }) === beforeDelayedRoomFit;
await waitMs(120);
result.singleRoomTapStillWaitsInsideWindow = !c._cameraTransition?.active
&& c._roomFocus === null
&& JSON.stringify({ zoom: c._zoom, view: c._view }) === beforeDelayedRoomFit;
await waitMs(250);
const tweenStarted = c._cameraTransition?.active === true;
const syncFrames = [];
if (tweenStarted) {
@@ -108,6 +119,7 @@ const out = await page.evaluate(async () => {
const settled = JSON.stringify(c._view);
pointer(roomNode(), 'pointerdown', 15202);
pointer(roomNode(), 'pointerup', 15202);
await waitRoomDecision();
await waitCamera();
result.repeatedFitIsNoOp = !c._cameraTransition.active && JSON.stringify(c._view) === settled;
@@ -158,10 +170,27 @@ const out = await page.evaluate(async () => {
result.areaLinkSuppressesRoomFit = JSON.stringify(c._view) === beforeLink
&& c._roomPointer === null && c._roomFocus === null;
c._clearRoomFocus(true);
pointer(roomNode(), 'pointerdown', 152041);
pointer(roomNode(), 'pointerup', 152041);
pointer(ownerProbe, 'pointerdown', 152042);
pointer(ownerProbe, 'pointerup', 152042);
await waitRoomDecision();
result.interactiveSecondTapCancelsPendingRoomFit = c._roomFocus === null
&& !c._cameraTransition.active;
c._clearRoomFocus(true);
pointer(roomNode(), 'pointerdown', 152043);
pointer(roomNode(), 'pointerup', 152043);
c._clearRoomFocus(true);
await waitRoomDecision();
result.lifecycleCleanupCancelsPendingRoomFit = c._roomFocus === null
&& !c._cameraTransition.active;
const stage = root.querySelector('.stage');
const base = c._baseVb();
const moveAway = async () => {
c._doubleFit.clear();
c._clearRoomFocus(true);
c._applyView(2, base[0] + base[2] * 0.65, base[1] + base[3] * 0.4);
await c.updateComplete;
};
@@ -211,14 +240,14 @@ const out = await page.evaluate(async () => {
result.cancelDisarmsThePreviousTap = c._zoom === 2 && !c._cameraTransition.active;
c._mode = 'decor';
c._doubleFit.clear();
c._clearRoomFocus(true);
pointer(stage, 'pointerdown', 44909, 400, 500);
pointer(stage, 'pointerup', 44909, 400, 500);
pointer(stage, 'pointerdown', 44910, 400, 500);
pointer(stage, 'pointerup', 44910, 400, 500);
result.editorBackgroundDoesNotFit = c._zoom === 2 && !c._cameraTransition.active;
c._mode = 'view';
c._doubleFit.clear();
c._clearRoomFocus(true);
pointer(roomNode(), 'pointerdown', 15205, 500, 300);
pointer(roomNode(), 'pointermove', 15205, 520, 300);
@@ -228,6 +257,7 @@ const out = await page.evaluate(async () => {
pointer(roomNode(), 'pointerdown', 15206, 500, 300);
pointer(roomNode(), 'pointerup', 15206, 500, 300);
await waitRoomDecision();
await waitCamera();
c.style.width = '760px';
c._lastValidStageSize = [1000, c._stageEl.clientHeight];
@@ -246,6 +276,7 @@ const out = await page.evaluate(async () => {
await c.updateComplete;
pointer(roomNode(), 'pointerdown', 15207);
pointer(roomNode(), 'pointerup', 15207);
await waitRoomDecision();
await waitCamera();
const isoMargins = margins();
result.isoUsesProjectedBounds = c._effectiveProjection() === 'iso'
@@ -267,19 +298,31 @@ const out = await page.evaluate(async () => {
pointerType: 'touch', isPrimary: true, button: 0,
buttons: type === 'pointerdown' ? 1 : 0, clientX: 450, clientY: 300,
}));
for (const id of [15208, 15209]) {
kioskPointer(kioskRoom, 'pointerdown', id);
kioskPointer(kioskRoom, 'pointerup', id);
const started = performance.now();
while (kiosk._cameraTransition.active && performance.now() - started < 1200) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
kioskPointer(kioskRoom, 'pointerdown', 15208);
kioskPointer(kioskRoom, 'pointerup', 15208);
const kioskSingleWaits = kiosk._roomFocus === null && !kiosk._cameraTransition.active;
await waitRoomDecision();
while (kiosk._cameraTransition.active) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const beforeKioskBackground = JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view });
result.kioskSingleRoomTapFitsAfterDelay = kioskSingleWaits
&& kiosk._roomFocus?.roomId === 'room-a';
kiosk._clearRoomFocus(true);
kiosk._applyView(2);
const beforeKioskPair = JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view });
kioskPointer(kioskRoom, 'pointerdown', 15209);
kioskPointer(kioskRoom, 'pointerup', 15209);
const kioskRoomPairHasNoIntermediateMotion = !kiosk._cameraTransition.active
&& JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view }) === beforeKioskPair;
kioskPointer(kiosk._stageEl, 'pointerdown', 15210);
kioskPointer(kiosk._stageEl, 'pointerup', 15210);
result.kioskRoomTapDoesNotEnterDoubleTap = kiosk._roomFocus?.roomId === 'room-a'
&& JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view }) === beforeKioskBackground;
result.kioskRoomThenBackgroundUsesDoubleFit = kioskRoomPairHasNoIntermediateMotion
&& kiosk._cameraTransition.state?.reason === 'double-tap'
&& kiosk._roomFocus === null;
while (kiosk._cameraTransition.active) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
kiosk._clearRoomFocus(true);
kiosk._applyView(2);
for (const id of [15211, 15212]) {
+152 -3
View File
@@ -85,11 +85,23 @@ const out = await page.evaluate(async () => {
const activeSpace = () => root().querySelector('[data-hp="space-tab"][aria-current="page"]')
?.getAttribute('data-id');
const closeTo = (a, b, tolerance = 1e-5) => Math.abs(a - b) <= tolerance;
const pathClose = (a, b, tolerance = 1e-5) => {
const numbers = (value) => String(value || '').match(/-?\d+(?:\.\d+)?/g)?.map(Number) || [];
const left = numbers(a);
const right = numbers(b);
return left.length === right.length
&& left.every((value, index) => closeTo(value, right[index], tolerance));
};
const stairStyle = (stair) => ({
color: stair?.color, opacity: stair?.opacity,
fill_color: stair?.fill_color, fill_opacity: stair?.fill_opacity,
});
const axisAngleDistance = (angle) => {
const normalized = ((angle % 180) + 180) % 180;
return Math.min(normalized, 180 - normalized);
};
const result = {};
let dialog;
await hp.setServerConfig((config) => {
for (const space of config.spaces) delete space.stairs;
@@ -130,6 +142,38 @@ const out = await page.evaluate(async () => {
result.renderedTypesAndDirections = !!root().querySelector(
'[data-hp="stair"][data-kind="straight"] .hp-stair-arrow',
) && !!root().querySelector('[data-hp="stair"][data-kind="spiral"] .hp-stair-arrow');
result.newStairsSnapshotCurrentDecorStyle = [straight, spiral].every((stair) =>
stair.color === card._decorStyle.color
&& stair.opacity === card._decorStyle.opacity
&& stair.fill_color === card._decorStyle.color
&& stair.fill_opacity === 0);
result.straightHasTrapezoidAndSpiralDoesNot =
stairNode(straight.id)?.querySelectorAll('.hp-stair-trapezoid').length === 3
&& stairNode(spiral.id)?.querySelectorAll('.hp-stair-trapezoid').length === 0;
// A legacy record remains untouched on read; its first explicit Save writes
// the complete visual quartet using the current decor fallback.
await hp.setServerConfig((config) => {
const legacy = config.spaces.find((space) => space.id === 'f1').stairs
.find((stair) => stair.id === spiral.id);
delete legacy.color;
delete legacy.opacity;
delete legacy.fill_color;
delete legacy.fill_opacity;
return config;
});
await hp.switchSpace('f1');
await hp.setMode('plan');
const legacyRead = stairs().find((stair) => stair.id === spiral.id);
result.legacyReadDoesNotMaterializeStyle = !Object.hasOwn(legacyRead, 'color')
&& stairNode(spiral.id)?.style.getPropertyValue('--hp-stair-line') === card._decorStyle.color;
dialog = await openStairDialog(spiral.id);
await saveDialog(dialog);
const materialized = stairs().find((stair) => stair.id === spiral.id);
result.firstLegacySaveMaterializesStyle = materialized.color === card._decorStyle.color
&& materialized.opacity === card._decorStyle.opacity
&& materialized.fill_color === card._decorStyle.color
&& materialized.fill_opacity === 0;
// Drag-to-draw (#676 AC1): a press-drag-release on the stage draws the
// straight stair at the drawn size with the ascent along the drag; the
@@ -302,13 +346,65 @@ const out = await page.evaluate(async () => {
// sizes above one metre survive and no history entry is written.
const untouched = JSON.stringify(stairs().find((stair) => stair.id === straight.id));
const historyBefore = card._geometryHistory.size;
let dialog = await openStairDialog(straight.id);
dialog = await openStairDialog(straight.id);
const fieldsShown = [...dialog.querySelectorAll('input[type="number"]')].map((input) => input.value);
await saveDialog(dialog);
result.untouchedDialogSaveKeepsSizes = JSON.stringify(stairs().find((stair) => stair.id === straight.id)) === untouched
&& Number(fieldsShown[0]) > 100;
result.untouchedDialogSaveWritesNoHistory = card._geometryHistory.size === historyBefore;
// Both standard colour controls are persisted together. Down flips only the
// trapezoid: the canonical arrow path does not move (#683 owner decision).
const arrowBeforeDirection = stairNode(straight.id)?.querySelector('.hp-stair-arrow')?.getAttribute('d');
const trapezoidBeforeDirection = [...(stairNode(straight.id)
?.querySelectorAll('.hp-stair-trapezoid') || [])].map((line) => line.outerHTML).join('');
const beforeCancelledStyle = structuredClone(stairs().find((stair) => stair.id === straight.id));
dialog = await openStairDialog(straight.id);
let colorPickers = dialog ? [...dialog.querySelectorAll('hp-color-opacity')] : [];
result.propertiesExposeBothColourControls = colorPickers.length === 2
&& colorPickers[0].label === 'Tread and arrow colour'
&& colorPickers[1].label === 'Fill colour';
colorPickers[0]?.dispatchEvent(new CustomEvent('hp-color-opacity-change', {
detail: { color: '#ff0000', opacity: 0.1 }, bubbles: true, composed: true,
}));
colorPickers[1]?.dispatchEvent(new CustomEvent('hp-color-opacity-change', {
detail: { color: '#00ff00', opacity: 0.9 }, bubbles: true, composed: true,
}));
await hp.close(dialog, { via: 'cancel' });
result.cancelKeepsStairVisualStyle = JSON.stringify(stairStyle(
stairs().find((stair) => stair.id === straight.id),
)) === JSON.stringify(stairStyle(beforeCancelledStyle));
dialog = await openStairDialog(straight.id);
colorPickers = dialog ? [...dialog.querySelectorAll('hp-color-opacity')] : [];
result.reopenRestoresPersistedStyle = colorPickers[0]?.color === beforeCancelledStyle.color
&& colorPickers[0]?.opacity === beforeCancelledStyle.opacity
&& colorPickers[1]?.color === beforeCancelledStyle.fill_color
&& colorPickers[1]?.opacity === beforeCancelledStyle.fill_opacity;
colorPickers[0]?.dispatchEvent(new CustomEvent('hp-color-opacity-change', {
detail: { color: '#123456', opacity: 0.7 }, bubbles: true, composed: true,
}));
colorPickers[1]?.dispatchEvent(new CustomEvent('hp-color-opacity-change', {
detail: { color: '#abcdef', opacity: 0.25 }, bubbles: true, composed: true,
}));
await settled();
dialog = root().querySelector('[data-hp="dialog"][data-kind="stairs"]');
const down = [...(dialog?.querySelectorAll('.segmented .btn') || [])]
.find((button) => button.textContent.trim() === 'Down');
down?.click();
await saveDialog(dialog);
const styled = stairs().find((stair) => stair.id === straight.id);
const styledNode = stairNode(straight.id);
result.coloursAndOpacityPersistAsOneEdit = styled?.color === '#123456'
&& styled.opacity === 0.7 && styled.fill_color === '#abcdef'
&& styled.fill_opacity === 0.25
&& styledNode?.style.getPropertyValue('--hp-stair-line') === '#123456'
&& styledNode?.style.getPropertyValue('--hp-stair-fill') === '#abcdef';
result.downFlipsOnlyTrapezoid = styled?.direction === 'backward'
&& pathClose(styledNode?.querySelector('.hp-stair-arrow')?.getAttribute('d'), arrowBeforeDirection)
&& [...(styledNode?.querySelectorAll('.hp-stair-trapezoid') || [])]
.map((line) => line.outerHTML).join('') !== trapezoidBeforeDirection;
// Properties switch kind without changing identity, then switch it back and link Garden.
dialog = await openStairDialog(straight.id);
const selects = dialog ? [...dialog.querySelectorAll('select')] : [];
@@ -322,12 +418,14 @@ const out = await page.evaluate(async () => {
await saveDialog(dialog);
const linked = stairs().find((stair) => stair.id === straight.id);
result.kindSwitchKeepsIdentityAndLink = converted?.kind === 'spiral'
&& linked?.kind === 'straight' && linked.target_space_id === 'garden';
&& linked?.kind === 'straight' && linked.target_space_id === 'garden'
&& linked.color === '#123456' && linked.fill_color === '#abcdef';
await hp.setMode('view');
let linkedNode = stairNode(straight.id);
result.validLinkIsAccessible = linkedNode?.getAttribute('role') === 'link'
&& linkedNode?.getAttribute('data-target-state') === 'active';
&& linkedNode?.getAttribute('data-target-state') === 'active'
&& getComputedStyle(linkedNode).cursor === 'pointer';
// Hover on a link announces the target floor; every other target state
// stays silent (#676 AC8). The mouse pointer type enables hover.
@@ -543,5 +641,56 @@ const out = await page.evaluate(async () => {
return result;
});
// #686: a focused stair link paints no focus frame — neither the browser's
// ring nor a replacement — and it still follows with Enter. The oracle is
// pixels: the stair's area with a margin for the ring is captured unfocused
// and with keyboard focus, and the two frames must be byte-identical.
const focusProbe = await page.evaluate(async () => {
const card = window.__card;
const hp = window.__hpTest;
await hp.setVolumetricView(false);
await hp.switchSpace('f1');
await hp.setMode('view');
await hp.settled();
const node = card.renderRoot.querySelector('[data-hp="stair"][data-target-state="active"]');
const rect = node?.getBoundingClientRect();
return node ? { id: node.getAttribute('data-id'), x: rect.x, y: rect.y, w: rect.width, h: rect.height } : null;
});
out.focusProbeHasActiveStair = !!focusProbe && focusProbe.w > 0 && focusProbe.h > 0;
if (focusProbe) {
const margin = 8;
const clip = {
x: Math.max(0, Math.floor(focusProbe.x - margin)), y: Math.max(0, Math.floor(focusProbe.y - margin)),
width: Math.ceil(focusProbe.w + 2 * margin), height: Math.ceil(focusProbe.h + 2 * margin),
};
await page.mouse.move(1, 1);
const shot = () => page.screenshot({ clip, animations: 'disabled', caret: 'hide' });
const unfocused = await shot();
// A real key press puts the page into keyboard modality, so the following
// programmatic focus matches :focus-visible exactly like Tab would.
await page.keyboard.press('Shift');
const focused = await page.evaluate(async (id) => {
const node = window.__card.renderRoot.querySelector(`[data-hp="stair"][data-id="${id}"]`);
node?.focus();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
return {
active: window.__card.renderRoot.activeElement === node,
visible: !!node?.matches(':focus-visible'),
};
}, focusProbe.id);
const withFocus = await shot();
out.keyboardFocusReachesStair = focused.active && focused.visible;
out.focusedStairPaintsNoFrame = focused.active && unfocused.equals(withFocus);
await page.keyboard.press('Enter');
out.focusedStairEnterNavigates = await page.evaluate(async () => {
const card = window.__card;
for (let guard = 0; card._cameraTransition?.active && guard < 90; guard++)
await new Promise((resolve) => requestAnimationFrame(resolve));
await window.__hpTest.settled();
return card.renderRoot.querySelector('[data-hp="space-tab"][aria-current="page"]')
?.getAttribute('data-id') === 'garden';
});
}
checkAll(out);
await finish(browser, out);
+149
View File
@@ -0,0 +1,149 @@
// Issue #685: a settled zoom must be a fresh vector frame after every route
// (button, wheel, pinch, every mode). #689 reverted #685's analytic gradient —
// the blur came from a frozen compositor raster, not from the hatch — so the
// wall hatch is again the one historical <pattern> at every scale.
import { createHash } from 'node:crypto';
import { launch, checkAll, finish } from './serve.mjs';
import { prepareGoldenScenario } from './golden/harness.mjs';
import { GOLDEN_SCENARIOS } from './golden/matrix.mjs';
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'opening-symbol-room-wall-light');
if (!scenario) throw new Error('opening-symbol-room-wall-light fixture is missing');
const sharpnessScenario = { ...scenario, extraOpenings: [{
id: 'sharpness-passage', type: 'passage', x: 0.62, y: 0.5, angle: 0, length: 0.06,
}] };
const { page, browser } = await launch({ width: 1000, height: 900 }, 1);
await prepareGoldenScenario(page, sharpnessScenario);
const settle = async () => {
await page.waitForFunction(() => !window.__goldenCard?._cameraTransition?.active);
await page.evaluate(async () => {
const card = window.__goldenCard;
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
};
const setZoom = async (zoom) => {
await page.evaluate((value) => {
const card = window.__goldenCard;
const vb = card._baseVb();
card._applyView(value, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
card.requestUpdate();
}, zoom);
await settle();
};
const state = () => page.evaluate(() => {
const card = window.__goldenCard;
const root = card.renderRoot;
const scene = root.querySelector('.plan-svg');
const view = card._viewOr(card._baseVb());
const exact = `${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 kinds = new Set([...root.querySelectorAll('.opening[data-kind]')]
.map((node) => node.getAttribute('data-kind')));
const modelKinds = new Set((card._openingsR || []).map((opening) => opening.type));
return {
zoom: card._zoom,
exactViewBox: scene?.getAttribute('viewBox') === exact,
noSceneTransform: scenes.every((node) => ['', 'none'].includes(getComputedStyle(node).transform)),
noSceneWillChange: scenes.every((node) => !getComputedStyle(node).willChange
|| getComputedStyle(node).willChange === 'auto'),
noLayerTransform: layers.every((node) => ['', 'none'].includes(getComputedStyle(node).transform)),
patternHatch: !!pattern && pattern.getAttribute('patternUnits') === 'userSpaceOnUse'
&& !root.querySelector('linearGradient#hp-wall-hatch'),
openingKinds: ['door', 'window', 'gate'].every((kind) => kinds.has(kind))
&& modelKinds.has('passage'),
};
});
const clip = await page.locator('houseplan-card').evaluate((card) => {
const rect = card.renderRoot.querySelector('.stage').getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const shotHash = async () => createHash('sha256')
.update(await page.screenshot({ clip, animations: 'disabled', scale: 'css' }))
.digest('hex');
// Button: 100 -> 140%.
await setZoom(1);
await page.locator('houseplan-card').evaluate((card) =>
card.renderRoot.querySelector('[data-hp="zoom-in"]').click());
await settle();
const button = await state();
const buttonHash = await shotHash();
// A different route to the exact same target: 196 -> 140%.
await setZoom(1.96);
await page.locator('houseplan-card').evaluate((card) =>
card.renderRoot.querySelector('[data-hp="zoom-out"]').click());
await settle();
const buttonReverse = await state();
const reverseHash = await shotHash();
// Wheel: one centred notch, 100 -> 115%.
await setZoom(1);
await page.locator('houseplan-card').evaluate((card) => {
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
stage.dispatchEvent(new WheelEvent('wheel', {
bubbles: true, cancelable: true, deltaY: -100,
clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2,
}));
});
await settle();
const wheel = await state();
// Pinch: 100 -> 132%, then release both fingers and wait for the terminal Lit
// frame. The intermediate promoted transform is allowed; the returned state is
// deliberately sampled only after the two full settled frames above.
await setZoom(1);
await page.locator('houseplan-card').evaluate((card) => {
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const emit = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true,
pointerId: id, pointerType: 'touch', isPrimary: id === 71,
button: 0, buttons: type === 'pointerup' ? 0 : 1,
clientX: rect.left + x, clientY: rect.top + y,
}));
const cx = rect.width / 2, cy = rect.height / 2;
emit('pointerdown', 71, cx - 100, cy);
emit('pointerdown', 72, cx + 100, cy);
emit('pointermove', 71, cx - 132, cy);
emit('pointermove', 72, cx + 132, cy);
emit('pointerup', 71, cx - 132, cy);
emit('pointerup', 72, cx + 132, cy);
});
await settle();
const pinch = await state();
// The same architectural SVG is shared by View and all three editors.
const modes = [];
for (const mode of ['view', 'plan', 'devices', 'decor']) {
await page.evaluate((value) => window.__hpTest.setMode(value), mode);
await page.waitForTimeout(350);
await settle();
await setZoom(1.32);
modes.push(await state());
}
const terminal = (sample) => sample.exactViewBox && sample.noSceneTransform
&& sample.noSceneWillChange && sample.noLayerTransform && sample.patternHatch
&& sample.openingKinds;
const out = {
buttonSettlesAt140: Math.abs(button.zoom - 1.4) < 1e-9 && terminal(button),
reverseButtonSettlesAt140: Math.abs(buttonReverse.zoom - 1.4) < 1e-9
&& terminal(buttonReverse),
sameScaleIsDeterministic: buttonHash === reverseHash,
wheelSettlesAt115: Math.abs(wheel.zoom - 1.15) < 1e-9 && terminal(wheel),
pinchSettlesAt132: Math.abs(pinch.zoom - 1.32) < 1e-9 && terminal(pinch),
allModesUseSettledPatternHatch: modes.length === 4 && modes.every(terminal),
};
checkAll(out);
await finish(browser, { ...out, buttonHash, reverseHash, button, buttonReverse, wheel, pinch, modes });
+1 -1
View File
@@ -1,4 +1,4 @@
// Значения форматирует HOME ASSISTANT (docs/STYLING-HOOKS.md §6, docs/LIVE-TEXT.md §2.1).
// Значения форматирует HOME ASSISTANT (docs/STYLING-HOOKS.md §6, docs/DECOR-EDITOR.md §5.2).
// Везде, где карточка печатает состояние ОДНОЙ сущности, она зовёт
// hass.formatEntityState (и hass.formatEntityAttributeValue для атрибута) —
// значит, работают display_precision, локальный разделитель дробной части и
+51 -2
View File
@@ -9,8 +9,7 @@ of the shipped integration.
stand picks it up automatically from this path
(`/opt/hp/bin/hp-update-dev.sh`, which calls the same script). The rest of the stand-only config
(template LQI sensors, alarm helpers, the smoke automation) lives in the
seeds on the stand host — see `docs/TESTING-DEMO.md` and the memory note
`houseplan-demo-stand`.
seeds on the stand host; the demo home itself is described below.
- `demo_guard/` — stand-only guard (2026-07-31). Visitors log in as an
administrator (the card editor is gated on `is_admin`) and kept restarting
@@ -56,3 +55,53 @@ refuses anything that does not have exactly one
manifests turned the Hassfest job of PR #9004 red on 2026-08-11, five weeks into
the review queue. `test/repo-hygiene.test.mjs` now fails if a second one
appears, so this cannot be rediscovered by a reviewer again.
## The demo home
**https://demo.houseplan.tech** — public, login `demo` / `demo` (an
administrator). **https://dev.houseplan.tech** — the closed stand behind basic
auth (access with the owner), auto-deploys `dev`. The public stand **resets
every hour** to a pristine synthetic home — break it freely, delete rooms,
upload plans, an hour later everything is back (the countdown is printed to the
DevTools console). Long-lived checks («the file is gone a day later», «survived
a restart») therefore cannot live on the stand, and restarting HA from inside is
blocked by `demo_guard`.
Demo home v2 is an anonymised layout of a real country house: dashboard
«House plan» (`/house-plan/0`; views Plan / Kiosk / Schema) and three spaces —
**Ground Floor** (Kitchen & Living, Hallway, Guest Bedroom, Guest WC, Boiler
Room, Sauna, Under-Stairs Closet, Outdoor Storage — hand-drawn, thick hatched
walls), **First Floor** (Kids Room A, Kids Room B, Upstairs Hall, Kids
Bathroom, Master Bathroom, Bedroom, Study — with a plan image backdrop) and
**Yard** (the Yard room plus a decor outline of the garage). 97 markers in
total: ~51 on Ground Floor, 23 on First Floor, 2 in the yard. Key demo devices:
the robot vacuum `vacuum.demo_robot` (dock in Under-Stairs Closet), leak
sensors (toggled by the helper `input_boolean.demo_leak`), smoke sensors (fire
by themselves every 10 minutes), the boiler and the water tank in Boiler Room
(value markers plus live text labels), the Front Door and Terrace locks,
curtains in Study, the garage gate in Yard, a TV with speakers, the kitchen
hood, the air conditioner in Bedroom, the composite Smart Plug, the
permanently unavailable Pantry Light, and the weather
`weather.demo_weather_south` (sun in the windows and the day/night cycle).
Everything is in English; the demo user's language is Auto, so the interface
follows the browser. **Smart Plug 2** is deactivated in the HA registry on
purpose — it is the ready-made example for the disabled-devices behaviour.
The public demo user is an administrator, so the full registry scenario can be
checked on the stand; a limited/read-only user needs the local harness or a
separate unprivileged user.
## What the stand cannot show
- Real Zigbee/Z-Wave hardware, real robots (Dreame/Xiaomi/Valetudo), a real
wall tablet.
- The HACS install/update path, YAML-mode Lovelace, reinstalling the
integration while keeping its config.
- Restarting HA from inside — blocked by `demo_guard` (the hourly container
reset is the only «restart»).
- Anything that needs the stand's file system: broken stores, `kill -9`
between writes, unreadable folders, watching process memory.
- Long-lived scenarios (a day-long sweep, «an hour later») — the reset comes
first.
- A non-admin user (`admin_only`, hidden tabs) — the stand has the single
admin `demo`.
+4 -1
View File
@@ -3,13 +3,16 @@
// Room settings form, and verifies the public ha-dialog geometry on desktop
// and both sides of HA's built-in 450 px mobile breakpoint.
import assert from 'node:assert/strict';
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { assertFreshDemoBundle } from './bundle-freshness.mjs';
import { launchHaDialogFixture } from './helpers/ha-dialog-fixture.mjs';
const capture = process.argv.includes('--capture');
const pairDir = fileURLToPath(new URL('../docs/design/600-settings-dialogs/pairs/', import.meta.url));
// `--capture` writes diagnostic images into ignored artifacts/, never into docs (#681).
const pairDir = fileURLToPath(new URL('../artifacts/ha-form-shell-609/', import.meta.url));
if (capture) mkdirSync(pairDir, { recursive: true });
const fixture = await launchHaDialogFixture({
authentic: true,
viewport: { width: 1600, height: 1000 },
+260 -260
View File
@@ -1,436 +1,436 @@
{
"schema": 1,
"fingerprint": "51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",
"fingerprint": "a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299153,
"initialViewGzipBytes": 300323,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 300675,
"initialPanelGzipBytes": 301845,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2324,
"initialPanelOnlyGzipBytes": 2325,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.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": 456031,
"lazyGzipBytes": 456389,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.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": 237970,
"lazyEditorGzipBytes": 238192,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/namespace-language-CIeeXPH2.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": 27944,
"lazyOnboardingGzipBytes": 27950,
"lazyLocaleFiles": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/fr-BLCP2_-N.js"
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/fr-CpDKyK4Z.js"
],
"lazyLocaleGzipBytes": 56294,
"lazyLocaleGzipBytes": 56330,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-SCzI9z9k.js"
"houseplan-assets/iso-scene-render-BxXtKC5L.js"
],
"lazyIsometricGzipBytes": 17882,
"lazyIsometricGzipBytes": 17885,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js"
],
"lazyFurnitureArtGzipBytes": 16941,
"lazyFurnitureArtGzipBytes": 16940,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/pdf-export-CGU5_sl_.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/pdf-export-DL7VmwpB.js"
],
"lazyPdfGzipBytes": 126914,
"lazyPdfGzipBytes": 126938,
"lazyNamespaceLocaleFiles": [
"houseplan-assets/settings-de-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js",
"houseplan-assets/support-de-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.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": 21037,
"lazyNamespaceLocaleGzipBytes": 21057,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CmogasSW.js",
"sha256": "aea6305d5506f017f2b0f8239cce47b10f2e02136b53b3002e2f708deab95444",
"path": "houseplan-assets/backdrop-pick-WDIF6iao.js",
"sha256": "5f6dcedf274594075d8acd31295d8c23f6d1408c890c086825db6bcbaa6c9b21",
"rawBytes": 54017,
"gzipBytes": 17574,
"gzipBytes": 17576,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.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-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js"
"houseplan-assets/settings-de-DfJ7EKF9.js",
"houseplan-assets/settings-fr-Bl79E-6s.js",
"houseplan-assets/settings-ru-CkNFmChi.js"
]
},
{
"path": "houseplan-assets/de-D7PZcyoc.js",
"sha256": "5e425bd4558bffbf4ad81d3a51e3ecd9fe7f76697482131b00ddeae65c0b37a4",
"rawBytes": 94126,
"gzipBytes": 28375,
"path": "houseplan-assets/de-E17v3gXt.js",
"sha256": "ed10d6c5e7a82b275c9422be67c2213219a75dba9aab0ddb3e98813249cbd1d0",
"rawBytes": 94196,
"gzipBytes": 28393,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CKG7G85d.js",
"sha256": "db6d125e73e1d2c8535645c576f7df912badd35bb93477d2a38537203c0dbba5",
"path": "houseplan-assets/editor-CbeYCXUv.js",
"sha256": "75887638c4d76040651a3cf52c56de10dc19e0cbc7583ee169d9b20cc5987788",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"sha256": "4252673f16e0382d3dd9ec967c8df782b2a315bb0658f05ad246f70e77aad222",
"path": "houseplan-assets/form-kit.styles-v5DlO12L.js",
"sha256": "cdab9e5e612be9b98c49193a2a6287080c13f62aaefa9a28b69bac94dd934012",
"rawBytes": 25600,
"gzipBytes": 5055,
"gzipBytes": 5056,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-BLCP2_-N.js",
"sha256": "b6f1cd483e2f0bb3c30ce2089b5e52fb65578e656496b2b9623b877b32339fa6",
"rawBytes": 96530,
"gzipBytes": 27919,
"path": "houseplan-assets/fr-CpDKyK4Z.js",
"sha256": "ee9fe5934eea69e549ff48d2e3fdabd7c20e0d9906a6f4623314f6eea48bf269",
"rawBytes": 96627,
"gzipBytes": 27937,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"sha256": "1152cf5d82e01ae897ef187661978a5d0c3a7bf49f8655c96ddaad564c191c29",
"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-BVZ7SgFj.js",
"sha256": "de63fea2ef07a50481acafb3236f8f02962895cc630b026910f76b2be4c431fd",
"rawBytes": 1055543,
"gzipBytes": 298351,
"path": "houseplan-assets/houseplan-card-BYwTiVnu.js",
"sha256": "055b6f8ba3a472be163a15ef6e2348727b5b03e3660b9362f3aed21d7be90426",
"rawBytes": 1059790,
"gzipBytes": 299520,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.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-CpIQitHC.js",
"sha256": "3fb964d9241252e3b00bedcc0c89f994aec183b3f657b6af38496b1f803db253",
"rawBytes": 687631,
"gzipBytes": 192112,
"path": "houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"sha256": "f25cbcb0453279e3ac64eda67df52af100d96c54bb93cb657bc08c355e0484ed",
"rawBytes": 688848,
"gzipBytes": 192327,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.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-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.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-BzvIlGC6.js",
"sha256": "2fcbe1523d5e570804a65c9138d7d2b41c951320af19a36844f9536b32218564",
"path": "houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"sha256": "4ea3584c8b2bfc86f96c587549ddce49eaf0b918923df2df8e98c9c0a548f835",
"rawBytes": 15886,
"gzipBytes": 4906,
"gzipBytes": 4908,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.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-CsVfJvR7.js",
"sha256": "bd59aa84c7afc96a1f50abaf8205bd745798264f27114a40cd7b334aabe1fd14",
"path": "houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"sha256": "2a0f9af108b8a39889f90d7119d8e7789716c9960ea8e28af38ada07cdc91a8c",
"rawBytes": 10958,
"gzipBytes": 3767,
"gzipBytes": 3765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.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-SCzI9z9k.js",
"sha256": "cf356172a58c90e29476142476c4d625b49d13c7414fbce151327b8b3626d40f",
"path": "houseplan-assets/iso-scene-render-BxXtKC5L.js",
"sha256": "4150eb9120fdaafa745761242ff3285056ceaccd7b016a16364574b90e3e2cfe",
"rawBytes": 52970,
"gzipBytes": 17882,
"gzipBytes": 17885,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"sha256": "a395a4dcb26e33285521d157e16bacff01f26da5f46158b30e6e25c48679644e",
"rawBytes": 8216,
"gzipBytes": 3208,
"path": "houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"sha256": "66e70baa00f4c9470ac9a37a229652cf5c5585caa2364de4d467a1c2ebe24358",
"rawBytes": 8370,
"gzipBytes": 3280,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/namespace-language-CIeeXPH2.js",
"sha256": "4bbd058806d1870d8d6b109f1a7c1a29e86b0ee2188a46441d38ba6719795964",
"path": "houseplan-assets/namespace-language-zIuNvgGV.js",
"sha256": "244bce2d5211106846215be6a9d3dacda4c910d06020144f9ffd5e432c5e8c00",
"rawBytes": 675,
"gzipBytes": 409,
"gzipBytes": 410,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CGU5_sl_.js",
"sha256": "0b1e4968ecf729dc267e03d63062bba53490e737635d9f3f1681a0320768e6d4",
"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-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-de-Crekk5Hk.js",
"sha256": "a769daa4cf5674115e7ed3172992b247e56f4ce72f883af46921dbb52f8512ad",
"path": "houseplan-assets/settings-de-DfJ7EKF9.js",
"sha256": "0580b9f6191edb664c21dee21e5263489eaf22876bcad67ef12e440abc03e28a",
"rawBytes": 7378,
"gzipBytes": 2879,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-Bl79E-6s.js",
"sha256": "f1bde9db7fe884aabd25130f783a84d8fe27bc9553923c17bcb9b82ee3976475",
"rawBytes": 7620,
"gzipBytes": 2842,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-CkNFmChi.js",
"sha256": "94f96c4a15a3a82ca8be3066629734379dd14eb2ff9e8f6acf369da2378676cb",
"rawBytes": 10304,
"gzipBytes": 3325,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-C_vpFucN.js",
"sha256": "d45dbdfa17a1da9f45bfbca6d6c3cc98849fd5ebcc06c8f72fd6310b04b93aad",
"rawBytes": 14862,
"gzipBytes": 3425,
"isEntry": false,
"imports": [
"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-D47J-OjO.js",
"sha256": "909d2195fcfca30c2f1279e07e2d7506d470c3daec6fe864c97f7ecc4101e33d",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
"sha256": "e44860a1af9ec7ffc7ac289c8c76ed1d0dcf0177bd91bbfe1981fdd0d4a62c46",
"rawBytes": 72959,
"gzipBytes": 20242,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-C_vpFucN.js",
"houseplan-assets/summary-panel-metrics-D47J-OjO.js"
]
},
{
"path": "houseplan-assets/support-de-DZoi-fxk.js",
"sha256": "f01d2d00a169519f5f402e99e9c85c9f21a23aeee2a9605d4444910f3250d6de",
"rawBytes": 6167,
"gzipBytes": 2396,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-Daosy89Q.js",
"sha256": "e503a1b4e1b1ed31d8fa09bfe0cdc069a82c1d1627d40407472b336fbc98ccf5",
"rawBytes": 6333,
"gzipBytes": 2432,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-e8-BrqtH.js",
"sha256": "76742313c4b510163c380f8fd9c7fcaf1cc7f8bde6a41bcb0da48621a7c3a3b3",
"rawBytes": 8376,
"gzipBytes": 2876,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-BzCuGnet.js",
"sha256": "40a596406b0bafbc3f2d9a54b12b9ac9e1713f939f98a630c0a576b53780d255",
"rawBytes": 7620,
"gzipBytes": 2839,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-BxY9uxyq.js",
"sha256": "929c462c195a7cf42c8cb2d4b558b4ae5ab1508eeafe4fb6a9e890f3650464ff",
"rawBytes": 10304,
"gzipBytes": 3323,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"sha256": "388fb3fcd97490eee5cdb7140953f7b421fcd06bee24c7f19e0e79d24b777bde",
"rawBytes": 14862,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js",
"sha256": "6c370c9858be82bdc1e77f77d598147be7673ac64656f533a62c0b15f6cfa5a9",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"sha256": "45c6b69dea88a448e81976da8cad5656bc7ee26ee49b3f9685d8bcba62d845a3",
"rawBytes": 72959,
"gzipBytes": 20243,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js"
]
},
{
"path": "houseplan-assets/support-de-9JIBMnMv.js",
"sha256": "83f703aba89eccb9e1b8ebf5e5b78d60521406a6a1ae7e4cc3a852a68f74b1c9",
"rawBytes": 6167,
"gzipBytes": 2395,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-B7V5UpZx.js",
"sha256": "3515c3761d2870728cac7760d871ccd03d9468d5001e43670b14a5b64126f6d3",
"rawBytes": 6333,
"gzipBytes": 2429,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-DVgRGpdZ.js",
"sha256": "794b61c69025bbe3cee78266b8833a5c22094930bfbd419fdea43fc38ca2f8cf",
"rawBytes": 8376,
"gzipBytes": 2875,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-de-Eo3zRC2b.js",
"sha256": "b73933a557179215bdae75cc51b2713541a032ed6c0240b51599f4ab73bf0ebb",
"path": "houseplan-assets/topology-de-Cs36ouVE.js",
"sha256": "7c808f1139bb1c232627bfe884a579177ec653d16147d6b96066851893b312a4",
"rawBytes": 2698,
"gzipBytes": 1333,
"gzipBytes": 1337,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-fr-BZMZ_i-b.js",
"sha256": "341977cab7561948330323c3da99cde1c56f9a225f9f78c154ce37b6af9f6a0c",
"path": "houseplan-assets/topology-fr-B8yoJGvc.js",
"sha256": "6ee1530fd0bd7a8c476c79b83404c7d8a5b98cd0b86ee8a73048be75a70cc2c4",
"rawBytes": 2718,
"gzipBytes": 1353,
"gzipBytes": 1354,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-ru-MwP3wic3.js",
"sha256": "f49389bc3c849476d3b97d38493262f8eb7a10aa6432dd6d555578f486b21f4a",
"path": "houseplan-assets/topology-ru-BRKShmrQ.js",
"sha256": "7fc89542fa860318d459ce1467a5a8931f3214a9d17f57c177ba01f2cfbbd3e8",
"rawBytes": 3669,
"gzipBytes": 1614,
"gzipBytes": 1616,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-CjcVZzSn.js",
"sha256": "b0136228d2845e59ef35b87fff2e19d28609fcef222ad2f46d6c71f186a9ece2",
"path": "houseplan-assets/zigbee-topology-DOAnbvlu.js",
"sha256": "313f71677ba0739d9c978185c2136e73e8037b770eb3f9ef8561f692d654406f",
"rawBytes": 10958,
"gzipBytes": 4296,
"gzipBytes": 4299,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": [
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.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--7WS-Nzl.js",
"sha256": "25dd84923fd69b9a594b2b74a191f8e6dba398a757f8a05f3ea7c527bcf27399",
"path": "houseplan-assets/zigbee-topology-runtime-CKlry6R8.js",
"sha256": "39e4a250de0e67e80d8afc02c39c1a79e42d4b3e113107bb3817f4fa90cab1e2",
"rawBytes": 3983,
"gzipBytes": 1788,
"gzipBytes": 1789,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.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": "7348cd78a4e1e27962b597d63f8d877291176a296357a03568557dda09df0903",
"sha256": "d4b480db1327152db7b1459c45692ea6b72fb7ac3bccb525bc06adfe1aab3b8d",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "54a23573ba2306bb0734b8b0ab051555d3646f68a2d8ad90798d0e2c69cc0abd",
"sha256": "5aaa677a9c85f4df0f176be31a91ebb5022f5ce129774919269bb0409c310233",
"rawBytes": 6165,
"gzipBytes": 2324,
"gzipBytes": 2325,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{aD as e,l as o,aG as t,ev as s,A as a,ew as i,ex as l,ey as n,E as r,c}from"./houseplan-card-BVZ7SgFj.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);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More