После первого движения камеры переносит фильтр контура на ограниченный размером viewport слой, сохраняя исходный статичный рендер без изменений.
Issue: #582
User-Visible: no
Режим заливки комнаты и её цвет — два поля конфига под одним переключателем.
«Как у пространства» снимало только режим; цвет оставался и продолжал
применяться, потому что `roomCustomFillOf` отдавал цвет комнаты независимо от
того, чей режим `custom` действует. Так возникало безымянное состояние «режим
наследую, цвет свой» — Cabinet на даче.
Теперь цвет комнаты участвует в раскраске только вместе с её собственным
`fill_mode: 'custom'` (одна функция — все поверхности: карточка, space-card,
PDF, черновик диалога). Диалог загружает цвет в черновик только при своём
режиме, обнуляет его при уходе с «Свой цвет» и показывает строку цвета только
под этим радио; сохранение пишет `custom_fill` только с `fill_mode: 'custom'`,
иначе удаляет — включая сироту от прежнего редактора. Чтение конфиг не
переписывает: застрявшие комнаты выздоравливают обновлением.
- `test/logic.test.mjs`: AC1 — сирота и любой чужой режим → цвет пространства
- `demo/smoke_room_settings.mjs` шаг 7: свой цвет → «Как у пространства» →
ни режима, ни цвета, во View цвет пространства; сирота открывается как
наследование, сохранение её удаляет (проверено красным на базе: 9 фактов)
- `demo/smoke_space_settings.mjs`: override с собственным режимом + сирота
- `demo/golden/harness.mjs`: `roomCustomFill` ставит комнате её режим —
кадры `lighting-custom-glow-*` не меняются
- мутант `room-orphan-colour-wins-again`
- docs: ARCHITECTURE (#56), CONFIG-COMPATIBILITY, USER-GUIDE ru/en, TESTING;
отпечаток скриншотов принят попиксельно (11 кадров)
Issue: #581
User-Visible: yes
Сводная панель теперь использует канонический порядок карточек Home Assistant и не переносит локальные настройки между визуальными колонками после remount.
Issue: #561
User-Visible: yes
The sidebar page could serve a previous card for hours. It imported the
card through the stable facade, `./houseplan-card.js` — a relative
specifier, and relative resolution does not inherit a query. A dashboard
reaches the same file as `houseplan-card.js?v=1.74.0`, so an upgrade
changes its URL and the browser must refetch. The panel always asked for
the same address, and entries are served with no Cache-Control at all —
only ETag and Last-Modified — so the browser applies heuristic freshness
and may answer from cache without asking. A stale 1164-byte loader names
a stale chunk, chunks are immutable for a year, and the panel then ran a
previous card against the current backend without a single error. The
version banner was telling the truth; reloading could not help, because
the address never changed.
Rollup already emits the right edge: the panel's side-effect import
points straight at the shared implementation. The rewrite in
entryFallbackPlugin replaced it with the facade for a fallback that the
hashed name gives anyway — and better: a chunk the manifest no longer
serves now raises the panel's own "House Plan was updated" notice
instead of silently working on old code.
Two #486 assertions change meaning and are rewritten, not adjusted: the
panel no longer routes through the facade, and its initial graph no
longer contains it. The invariant they defended — the panel reuses the
exact card graph, never a second copy — is now stated over the
implementation, and a new test pins that no built entry reaches the card
by an address without a version.
Issue: #535
User-Visible: yes
Перезапись `viewBox` — это не сдвиг, а инвалидация растеризации всей сцены.
Кадр жеста делал её каждый раз: в профиле владельца (Firefox 155, 144 Гц) кадр
доезжал до экрана 200 мс, а драйвер пропускал 124–144 тика в секунду с пометкой
«ждём краску».
Теперь `paintLiveViewport` держит якорь — кадр, чей `viewBox` записан в DOM, и
момент записи. Кадр жеста двигает узлы сцены тем же проективным преобразованием,
которым уже двигались HTML-слои, а `viewBox` переписывается по бюджету: 100 мс
либо 15 % сдвига/масштаба. Ни атрибут, ни стиль не пишутся, если строка не
изменилась.
Issue: #531
User-Visible: yes
`test/config-adoption-ownership.test.mjs` pins identity writes to the owner
and ratchets body staging (AC1/AC2). `demo/smoke_post_write_adoption.mjs`
drives space/delete (both runtimes), Optimize Undo and Import apply with a
concurrent backdrop change between the write and the re-read (AC4). Smokes
that seed revisions from outside the card keep working through the
`seedIdentity` harness seam behind the card's delegate setters. The initial
View ceiling is re-centred with the measured fact; ARCHITECTURE.md gets the
boundary paragraph.
Issue: #500
User-Visible: no
In HA hp-dialog renders ha-dialog, whose own `.body` is the scroller and
is not a flex container; `.summary-editor` (overflow:auto,
overscroll-behavior:contain, min-height:0) therefore grew to its content
and became a scroll container that never scrolls — Chromium stops wheel
and touch scroll chaining at such a child, so nothing moved (reproduced
on ha.jbstudio.pro, HA 2026.9.1, and in an isolated Playwright page).
hp-dialog gains an opt-in `flex-content` attribute forwarded as
ha-dialog's `flexcontent`, which lays the body out as a flex column: the
editor is height-bound again and scrolls itself, header and footer stay,
exactly as the native branch already did. Only the summary dialog opts in.
Smoke demo/smoke_summary_dialog_scroll.mjs stubs ha-dialog after the HA
2026.9 contract: wheel on desktop, touch swipe on a phone, dialog within
the viewport, and a witness that the stub reproduces the bug without
flexcontent. Docs screenshots: 11/11 pixel-identical (docs:accept
--identical, #512), fingerprint refreshed.
Mutants: summary-dialog-drops-flex-content, hp-dialog-ignores-flex-content.
Issue: #508
User-Visible: yes
Every card instance attached its summary-panel runtime through
import().then(), even when the chunk had loaded long ago. The first render
therefore measured a header without summary controls; the controls arrived
a beat later, the stage shrank, the deferred refit opened a `stage-resize`
continuity candidate and the first HA tick paid three extra render passes
(Full Performance: blend stateUpdate1 50 → 130 ms, overlay 217 → 809 ms;
locally 4 performUpdate per tick instead of 1).
summary-runtime-loader.ts separates the summary code from its state: the
loaded factory is cached per page, every host builds its own runtime from
it (no shared preferences, drafts, subscriptions, timers or DOM). A warm
factory yields the runtime synchronously in connectedCallback, before the
first Lit render; a cold mount still pays one lazy import, concurrent cold
mounts share the pending import, a failed import is forgotten so the next
connection retries, and a disconnect cancels the pending attachment of that
connection. SummaryRuntimeSlot owns the per-host lifecycle so the card core
stays under its line ceiling.
Witnesses: loader unit tests (distinct instances, shared pending import,
cancelled attachment, retry after failure); demo/smoke_summary_warm_attach
(warm replacement and cold-key instance on a warm page own the runtime
before the first render, header/stage stable from the first frame, no
stage-resize, one performUpdate per geometry-neutral tick, a real viewport
resize still opens stage-resize); mutant summary-runtime-attaches-after-
first-render. smoke_summary_panel waited for `_summary` as a readiness
proxy; it now waits for the server config load, which stays asynchronous.
Local paired glow benchmarks (4× CPU throttle): blend 159.7 → 49.9 ms and
overlay 326.7 → 120.4 ms at stateUpdate1 with renders 4 → 1; the isometric
load loses the three summary-owned long tasks.
Docs screenshots: all 11 frames decode pixel-identical to the committed
ones; the manifest carries only the new source fingerprint. Initial View
ceiling recentred 299 100 → 299 600 for the +299 B loader.
Issue: #506
User-Visible: yes
Keep the raised overlay footprint as calculation-only geometry for collision, nudge and fit while removing its painted SVG surface and texture. Update Stage 3 contracts, smokes, golden harness rules and synchronized bundles.
Issue: #471
User-Visible: no
(1) Corner and edge handles carry the same hit radius (1.8 % of the
view), so on furniture narrower than 4·hr — a 40 cm cabinet — the two
circles overlap and whichever is painted last takes the tap. Edges were
painted last. Corners are now, because a side handle scales one axis
while a corner scales both, and the object is small exactly when
proportional resize matters most. The visible beads are unchanged.
The audit called this 'proportional resize becomes unavailable'; the
measurement says otherwise and the spec records the correction: the
corner centre lies outside the edge circle, so the corner was reachable
— its area was halved, not lost. A polish, not a bug, and worth fixing
because it is one line of ordering.
(2) Alignment guides in the devices mode excluded the dragged marker by
_drag, which has been null there since #74 moved device dragging into
_deviceDrag. So the marker being moved was among its own candidates.
Nothing looked wrong because a point always matches itself within
tolerance — the guide was drawn from the marker to itself, visually
identical to an honest one, and the smoke asserted only guides() >= 1.
The smoke now compares the candidate lists with and without the drag and
demands exactly one removed entry.
(3) The 38 settings-help strings stay in the initial chunk, and that is
now a recorded decision rather than an oversight: measured 2 654 B gzip,
0.9 % of the ceiling, against splitting a synchronous dictionary in two,
a second request on first hint, and a second source for the key type
derived from en.json (#391). docs/ARCHITECTURE.md says so, with the
number that would justify revisiting it.
Both mutants run by hand: reverting the paint order reddens the 40 cm
probe while the 160 cm one stays green; restoring _drag reddens the
guides smoke.
User-Visible: yes
Issue: #400
The guard introduced by #394 matched the literal
sys.modules['custom_components... and therefore never looked at
pure_imports.py, which writes through a variable — the third instance of
the #389 class walked straight past the check created for it.
The guard now inspects the write itself and decides by the key: a whole
literal or the literal head of an f-string is safe unless it starts with
custom_components (that is how tests register homeassistant.*, hp_pure.*
and houseplan.trails); anything else — a variable, a concatenation,
setdefault/update — counts as a violation whenever the file is able to
name the package at all, i.e. contains a custom_components. literal. A
file that never names the package cannot poison it through a variable,
so restoring a snapshot stays legal.
load_pure now removes what it registered. Removing its own name is not
enough: relative imports pull neighbours in, so junction_limits leaves
wall_segment_model and coordinate_canonicalization behind. It removes
the whole custom_components difference accumulated during exec_module,
in a finally, and a repeated call still works.
pure_imports.py is a named exemption of the static guard precisely
because that guard cannot see the cleanup — so the cleanup is proven by
an executable test instead, and the mutant pure-imports-stops-cleaning
reddens it. Both mutants were run by hand.
User-Visible: no
Issue: #398