The shadow of a device marker is sized from the marker, and the marker is
sized from the container: --device-shell-shadow is expressed in
--dev-size, which resolves to 2.5cqw. With box-shadow in the transition
list, every container-query re-evaluation — a tooltip, a scrollbar, a
rotation — produced a new computed value and restarted a 150 ms
non-composited transition on every marker at once.
The owner's Firefox profile shows what that costs: 244 box-shadow
transitions, all on span.device-shell-frame, all oncompositor:false, in
bursts of exactly 61 (the markers on screen), four bursts in two
seconds. During them the tab presented 103 frames in 11 seconds — 9.4
per second, CONTENT_FRAME_TIME median 149 ms and up to 320 — while the
refresh driver waited for paint 381 times. Our JavaScript in the worst
three seconds: 16 ms. Chromium starts the same transitions (measured:
one pixel of container width starts two per marker in both engines) and
merely pays less for them, which is why this hid there.
The shadow itself is unchanged; it simply applies at once. The core
keeps the same treatment, so the selection and focus rings appear
without a fade — an instant ring is ordinary feedback, a faded one costs
a full-frame repaint per marker. Hover still animates border-color.
Issue: #524
User-Visible: yes
Lit reuses list nodes by position. The opening list and the device markers had
no keys, so on a space switch the leaf that held a slot kept its DOM node and
only changed values — and `.op-leaf` (transform) and `.op-arc`
(stroke-dashoffset) carry a 0.6 s transition, so the browser animated a door
that never moved: the new floor's leaf drove in from the previous floor's
opening angle. Measured on two spaces with a door in the same place and
opposite contact states: the node is reused, transform goes
`rotate(-90deg) → rotate(0deg)`, dash offset `0 → 125.66`, both transitions
`running`. The marker shell adds two more with its `box-shadow`.
Both lists are now rendered through `repeat(…, (item) => item.id, …)`, the
same lesson `glow-scene.ts` already learned for the Glow spots. The trap is
written where it starts — above the two transitions in `plan.styles.ts` —
because that is the file someone edits when adding the next animated property.
`houseplan-card.ts` is at its line ceiling, and the note would have cost the
budget a dozen lines for nothing: the swap itself is line-for-line.
The witness walks the shadow tree per element. `document.getAnimations()` is
empty here EVEN ON THE BROKEN CODE — the card lives in a shadow root and the
document-level call does not reach into it, and the issue proposed exactly
that call. The smoke also builds its own fixture: the demo home has no
openings at all, so two doors in two spaces are prepared in the smoke, and it
asserts the other half of the contract as well — a real contact change inside
one space still animates the leaf.
On `origin/dev` the smoke fails on five facts, naming the offenders:
`op-arc:stroke-dashoffset`, `op-leaf:transform`, `device-shell-frame:box-shadow`
twice. Mutants `openings-rendered-without-keys` and
`device-markers-rendered-without-keys` put each `map` back.
Perf, 7 samples against `19e421b3`: spaceSwitchMs 524.8 (limit 769.35, base
512.9), switchCycleMs 1293.9 (1696.28, 1256.5), firstStableRenderMs 2533.8
(3000, 2529.2), modelReadyMs 732.7 (944.97, 726.9), longTask.maxSingleMs 663
(910, 660) — `benchmark:compare` green in full.
The initial View graph grows 241 B gzip: `repeat` enters it for the first
time. The #438 ceiling is recentred 300 400 → 300 700 with the usual dated
note; measured 300 059 B keeps 641 B above and 1 359 B below the band. The
301 066 B budget is untouched, but only 366 B now separate the ceiling from
it — the #367 headroom debt has stopped being theoretical.
Issue: #525
User-Visible: yes
The candidate was assembled before #520 and #521 landed. Version fields are
already at 1.74.0-beta.1 in all seven sources and the bundles rebuild
byte-identical, so this commit only moves what the beta says about itself:
the #521 changelog entry leaves Unreleased for the beta.1 section in both
languages, the release notes gain its bullet on both sides (four and four,
the grouped small-fixes bullet stays last), and STATUS records the two
issues merged since — the alignment guides that follow the gesture again
(#521, a regression shipped in v1.72.0 and v1.73.0) and the cold-start
adoption fix behind it (#520, internal).
No product change: `npm run build` plus `bundle-sync` leave `dist/**` and
the two mirrored trees untouched, `node scripts/release-contract.mjs
v1.74.0-beta.1` is green on all seven version sources, `npm test` 2515/0.
The `Release:` trailer is what asks CI for the heavy gates — smokes, golden
and the full performance comparison — on this exact SHA (#479).
Issue: #521
Issue: #520
User-Visible: no
Release: v1.74.0-beta.1
#451 moved every editor gesture onto the live painter, and the guides stayed
behind in the settled scene. While a gesture runs, the settled scene is not
re-rendered at all, so the guides did not follow the marker in the device
editor, the shape in the backdrop editor, or the cursor while a contour is
drawn in the plan editor. Measured with real pointer events on the demo stand
against `origin/dev`, after waiting for the editor chrome to settle: three
gestures, each exactly on another object's axis, 0 settled render cycles,
`.alignline` 0 and no `.alignguides` group in all three.
The report called it two breaks. It is one — the layer — plus one thing that
would have broken the repair: `_alignPoint` read `_pos`, which during a live
gesture answers from the snapshot of the last settled render. Over one drag:
live 254.17 → 220.83 while `_pos` stayed at 254.17, eight grid steps behind,
so a restored layer would have drawn the guide at the marker's old place.
The live template now paints the guides in all three modes (the device editor
had no template at all — `paintDevice` only moves the marker element), and
`_alignPoint` takes the live position. The settled copy of
`.hp-editor-only-layer` is made transparent for the duration of any editor
gesture, not only in plan mode: two guides, one of them stale, is what the
user would otherwise see when an unrelated settled render lands mid-gesture.
`_renderAlignGuides` on the card becomes soft — a gesture that starts while
the editor runtime is still loading must cost nothing, and an exception inside
a `requestAnimationFrame` paint would take the whole gesture with it.
The witness is rewritten around the defect that hid this for two stable
releases: the old smoke assigned `_deviceDrag`/`_decorDraft` wholesale, and an
assignment with `oldValue == null` does not route to the live path — it
verified a state a real gesture never reaches. Every scenario now drives real
`PointerEvent`s, waits for silence first (the `_hdrH` settling window right
after entering a mode hands out settled frames that make even the broken code
draw a guide), and asserts zero settled cycles during the movements plus
exactly one `.alignguides` group. #400's exclusion is checked without touching
the drag state: the dragged marker must simply be absent from the candidates.
On `origin/dev` the smoke fails on nine of its facts; a witness that stays
green before the fix was the actual bug here.
Mutants: `live-editor-devices-drops-align-guides`,
`live-editor-decor-drops-align-guides`, `live-editor-plan-drops-align-guides`,
`align-point-reads-frozen-snapshot` — one per AC, all guarded by the smoke.
`test/smoke-harness-contract.test.mjs` pins that the smoke cannot go back to
fabricating gesture state.
Issue: #521
User-Visible: yes
Version fields and the generated bundles move to 1.74.0-beta.1 and open the
line after the stable v1.73.0. The changelog entries of #509 and #508 leave
Unreleased for the beta.1 section; release notes and STATUS describe them and
name the internal work of this beta. No product change beyond what is already
reviewed and merged.
Issue: #509
Issue: #508
User-Visible: no
Release: v1.74.0-beta.1
Two halves of the same first paint. The panel showed «Source unavailable»
in every row until the lazy metrics chunk arrived, because value() could
not tell "not loaded yet" from "source is dead"; and metrics() ran inside
render, walking the HA registry and unioning the clean floor of every
space synchronously — 11 s on the large-house fixture.
- totalCleanFloorAreaM2 computes the space's masonry and junction
topology once per SPACE and hands them to innerContourForRoom, which
otherwise unions the whole space again for every room: 11 045 → 1 488 ms
on that fixture, same 306.3 m². The card has always done this through
its own _innerContour cache; the panel now does the same.
- Aggregates leave the render path: the first frame paints skeletons and
the work starts right after the frame is shown (timeout → rAF →
timeout, never requestIdleCallback, which under load would leave the
skeleton up for seconds). Stale memo keeps the previous number on
screen instead of flashing back to a skeleton.
- valueState() separates pending from unavailable; a pending row keeps
the same plate, grid and height and carries a pulsing rectangle the
height of the line, replaced by the value with a short fade. Reduced
motion keeps the rectangle and drops the pulse.
- Panel enter/exit animation (#505, 190 ms) is now actually visible —
the main thread is free — and the smoke witnesses it.
Mutants: summary-first-paint-shows-unavailable, summary-metrics-block-first-frame,
summary-area-recomputes-walls-per-room, summary-stale-metric-falls-back-to-skeleton.
Issue: #509
User-Visible: yes
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
Version fields and generated bundles move to 1.73.0-beta.9; the #506
changelog entry leaves Unreleased for the beta.9 section; release notes and
STATUS describe the startup-regression fix that unblocks the stable
promotion. No product change beyond #506, already reviewed and merged.
Issue: #506
User-Visible: no
Release: v1.73.0-beta.9
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
Promote the eight published v1.73.0 betas without new product behavior. Synchronize version fields and generated bundles; aggregate public release notes from v1.72.0. Main-only workflow mirrors were merged with an identical beta.8 product tree.
Issue: #505
User-Visible: no
Release: v1.73.0
Restore the shared footer minimum exposed by the #505 Linux screenshot gate; the focused browser assertion fails before the restoration and passes after it.
Issue: #505
Issue: #495
User-Visible: yes
Attachment uploads stage the body as `.upload-*` under files_root and then
asked the quota to count that file as stored usage *and* as the incoming
size, so the last file that still fit was refused at the boundary — by
bytes and by count. check_quota/dir_usage now take `exclude` for the
caller's own staged file; other staged files keep counting, so two
concurrent uploads can never both land past the limit.
The support package copied every string key of settings.fill_colors. The
schema stays open for compatibility, but the projection now keeps only the
eleven slots the card defines (SUPPORT_FILL_COLOR_KEYS, pinned to
src/logic.ts DEFAULT_FILL_COLORS by a test); an empty palette is omitted.
The SVG local-reference walk was a recursive DFS: a flat chain of a few
thousand hrefs passed every #436 bound and died with RecursionError, which
the upload view turned into a 500. The walk is iterative and measures the
longest chain through each node (memoised, order-independent); chains
deeper than MAX_SVG_REF_DEPTH = 64 are refused as too_large, cycles stay
invalid_image.
Tests: quota boundaries on the validator and the HA endpoint, a barrier
test for concurrent uploads, palette allowlist and TS parity, reference
chains (plain, hostile id order, cycle) on the validator and the endpoint;
six mutants caught by the standard runner.
Issue: #498
User-Visible: yes
Apply re-validated the preview token after both halves were durable, so a
TTL that lapsed during the write, or an eviction by a newer preview of the
same user, answered "preview expired" for a plan that was already replaced
and withheld both update events. The token is now simply spent after the
commit; validity is decided once, on entry under write_lock.
Route runs dropped by drop_unknown_routes never reached the store unless a
marker happened to be orphaned in the same pass; an idle robot kept them in
memory only and a restart brought them back. The drop now happens under
_refresh_lock, leaves with the orphan transaction or with an immediate
write of its own, and rolls back like #335 when the store refuses.
Tests: HA harness for both apply races and a live config/set route drop,
recorder stubs for the four durability cases; five mutants caught by the
standard runner.
Issue: #495
User-Visible: yes
HA assigns panel/hass/narrow/route before the top-level-await entry has
defined the element, so the values landed as own properties that shadowed
the accessors and the card never received hass. And <ha-panel-custom> has
no height, so the percentage host height collapsed the stage to 0 px.
Adopt pre-upgrade properties through the accessors and size the panel from
the viewport minus HA's safe-area padding. The smoke now reproduces HA's
real mount order and container; two mutants guard both contracts. The
bundle is rebuilt from these sources.
Issue: #488
User-Visible: yes
Promote the published v1.72.0 beta line without new product behaviour: stable version fields, synchronized generated bundles, bilingual changelogs, release notes and status metadata only.
Issue: #54
Release: v1.72.0
User-Visible: no