Golden-джоба запускается только на кандидате с трейлером `Release:`, поэтому
Validate на мерже #530 её не гонял, и первый же релизный прогон
v1.74.0-beta.3 показал расхождение ровно в трёх сценах экспорта PDF:
`pdf-export-geometry-light`, `pdf-export-polish-light`,
`pdf-export-stepped-dimensions-light`.
Расхождение — это и есть предмет #530: план на листе крупнее, подписи
размеров стоят на чертеже, столбца выносов сбоку больше нет
(`sceneCoverage` 0.577). Кадры приняты с явным `--expect-change` из
артефакта того самого прогона (отпечаток исходников совпадает),
остальные 166 сцен сохранены без изменений, свидетелей среды 133.
Issue: #530
User-Visible: no
Release: v1.74.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34597337319
Перезапись `viewBox` — это не сдвиг, а инвалидация растеризации всей сцены.
Кадр жеста делал её каждый раз: в профиле владельца (Firefox 155, 144 Гц) кадр
доезжал до экрана 200 мс, а драйвер пропускал 124–144 тика в секунду с пометкой
«ждём краску».
Теперь `paintLiveViewport` держит якорь — кадр, чей `viewBox` записан в DOM, и
момент записи. Кадр жеста двигает узлы сцены тем же проективным преобразованием,
которым уже двигались HTML-слои, а `viewBox` переписывается по бюджету: 100 мс
либо 15 % сдвига/масштаба. Ни атрибут, ни стиль не пишутся, если строка не
изменилась.
Issue: #531
User-Visible: yes
It looked for the tag written as `css` immediately followed by a
backtick. The plugin is a Rollup transform, so the module has already
been through TypeScript by the time it arrives, and the TS printer puts
a space there: `css `. The guard therefore returned null for every
stylesheet in the project, and minification never ran once — around
23 KB of explanatory comments went to every user in every release.
Matching the tag as a word with optional whitespace turns it on:
chunk, raw 1 079 508 -> 1 021 115 B (-58 393)
initial view 300 111 -> 287 284 B gzip (-12 816)
room to the budget 955 -> 13 782 B
The ceiling moves down with the fact, as the tool asks when a graph
shrinks past the band.
The risk is not the two lines; it is that 23 KB of CSS is minified for
the first time. Two witnesses cover it: a browser smoke that puts the
original and the minified text into separate stylesheets and compares
the serialised rules — 1 049 of them, identical up to the whitespace
policy the minifier declares — and a test that takes real comment text
out of src/styles and requires it to be absent from dist, so a plugin
that silently stops working cannot pass again.
Issue: #526
User-Visible: yes
The marker half of the space-switch witness asked whether a box-shadow
transition was running on the shell. That worked only because such a
transition existed; #524 removed it — the shadow is sized in container
units and animating it cost a real user 9.4 frames per second — and the
check became trivially true. The mutant that removes the keys from the
marker list has been surviving ever since, and nobody noticed until the
next gate ran it.
The witness now keeps references to the marker nodes and requires that
none of them stays in the DOM under a different data-id after the
switch. Node identity is what the keys are for, and it does not depend
on any stylesheet.
The door half is untouched: there the transition is part of the product
contract, not a side effect.
Issue: #528
User-Visible: no
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
Code review r1 was right that AC5's evidence was empty: the smoke never
forced a settled render during a gesture, so the settled copy of
`.hp-editor-only-layer` was empty at every point it looked, and
`groups() === 1` held whether the copy was hidden or not.
Measuring the case the reviewer named turned up more than a weak assertion.
Ownership of the layer alternates on its own — every settled render ends in
`updated()` → `_commitLiveEditor()`, which empties the live root — so a
settled render mid-gesture takes the guides back and draws them itself, from
the same live `_alignPoint`. That much needs no suppression. But the copy it
leaves behind stays in the settled scene, and the NEXT live paint adds a
second one: measured two `.alignline` on one alignment, the settled one a
grid step behind the marker. So the suppression stays, and now it stays with
a witness.
The smoke counts what is visible, not what is in the DOM: the hidden copy is
still a node, and counting nodes is how this check could have looked green
while showing the user two lines. Its device scenario now drives the whole
handover — force an unrelated settled render mid-drag (`_hdrH`, the same
header-height observer that masked the defect in the S2 measurements), assert
the render actually happened, that the layer went back to the settled scene
with the live point on it, and that one real move later the live painter owns
it again — exactly one visible guide at every step.
Mutant `live-editor-keeps-the-settled-guides-visible` puts the suppression
back under the plan branch, as it was before this issue, and the smoke goes
red on `nextMoveTakesTheLayerBack`.
Issue: #521
User-Visible: no
#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
#500 gave `_serverCfg` and `_layout` prototype accessors but left them in
`static properties`. Lit marks such a property `wrapped` and, on the FIRST
update, force-writes it into `changedProperties` with an `undefined` old
value even though nobody assigned anything (`reactive-element.js:249-252`
and `:880-886`). `willUpdate` reads that as a config replacement, raises
`_cfgEpoch`, the memoized model key changes, and a 60-room house builds and
paints its model a second time: measured 19 update cycles, 4 builds and 4
epochs against 18 / 3 / 3 before #500, worth ~550 ms of `modelReadyMs` and
the same on `firstStableRenderMs` (3355 against a 3000 ceiling).
The declaration goes; the bodies stay reactive through the owner —
`_adoption` → `onBodyReplaced` → `requestUpdate(field, previous)` — which
needs no declaration: `getPropertyOptions` falls back to the default and
`changed.has('_serverCfg')` works as before. `noAccessor: true` would not
help, `wrapped` is set before that flag is read. The trap is written above
`static properties`, where someone would put the declaration back.
`cache.entries.cleanFloor` returns to 100 in both interaction budgets: the
120 entries were the extra epoch re-keying the per-room cache, not a
property of the design — the reasoning in 914e8402 was wrong.
Witness: test/config-adoption-ownership.test.mjs pins that neither body is
declared; the mutant `adoption-bodies-declared-reactive` puts the
declaration back and reddens it.
The boot diagnostics of the previous three commits touch four private
members, so they are declared in the performance contract: `_buildModel` and
`_cfgEpoch` outright (both exist in every supported comparison base), and the
adoption entry point as a current/legacy pair — #500 turned the private
`_adoptStructuralResponses` into the public `_adoptAuthoritative`, and an
undeclared rename would have the counter report zero adoptions instead of
failing.
The same commits carried a `node_modules` symlink: `.gitignore` had the
pattern with a trailing slash, which does not cover a symbolic link, and
`git add -A` in a sandbox worktree committed it. The link is removed and the
pattern loses the slash; a mutant run on this branch failed with `EEXIST` on
it.
Issue: #520
User-Visible: no
The comparison already names the mechanism: base does 18 update cycles, 3
model builds and ends at epoch 3, the candidate does 19, 4 and epoch 4, and
the extra build is the whole ~550 ms. What is still missing is the caller.
The diagnostic now installs an instance-level setter over `_cfgEpoch` and
records `from->to` with the top stack frames, so the extra bump names itself.
Issue: #520
User-Visible: no
The first attempt printed them with console.log inside page.evaluate, and
nothing forwards the page console to Node — the numbers went nowhere. The
sample now returns `bootDiag`, the runner prints it and strips it before the
row is recorded, so the budgeted record keeps its shape.
Issue: #520
User-Visible: no
The full comparison says model readiness grew by ~500 ms inside #500 and
that the growth sits in one long task, but neither contentFingerprint
(2.8 ms on this fixture) nor spaceModels (0.1 ms) can account for it. The
benchmark now prints, per sample, how many Lit update cycles ran before the
first stable frame, how long they took together, how many models were built,
how many adoptions happened and the config epoch. Diagnostics only: printed
to the log, never part of the budgeted record, and the same harness runs
against the comparison bundle, so candidate and base are counted alike.
Issue: #520
User-Visible: no
The pre-release perf gate of the v1.74.0-beta.1 candidate reported
cache.entries.cleanFloor 120 against a ceiling of 100 (run 34480302982,
large-house-interaction-v1). The ceiling was calibrated when only the visible
space populated `_cleanFloorCache`; since #509 the summary panel computes the
clean-floor total in per-room slices through the same cache, so the sixty
fixture rooms are cached under both config epochs the interaction profile
creates — 60 × 2 = 120, exactly what the run measured.
The ceiling moves to 180 in the smoke and in the full interaction profile:
one entry per room per epoch with room for a third epoch, far below the LRU
cap of 600 and far below anything a per-frame or per-marker regression would
produce. The leak detector is untouched: cacheGrowth.cleanFloor stays 0.
Issue: #509
User-Visible: no
Release: v1.74.0-beta.1
Review r2 M1. The smoke's reset() left the previous scenario's debounced
config/layout write pending; _deleteSpace flushes whatever is pending
before it writes, the fake socket answers without a rev, and the
documented rev+1 fallback then moved the revision on a body from another
scenario. The refused branch looked as if it had adopted:
onboardingDeleteRefusedAdoptsNothing was red on every run. The scenarios
are supposed to be independent, so reset() now cancels both debounced
writers, as smoke_danger_confirmation already does.
37/37 green, three runs in a row; with the cancel removed the same
single check goes red again.
The refused-tail fix from r1 had no witness in CI at all: no mutant
named this smoke as its guard, so the review gate never ran it and a red
witness survived a whole round. A witness that never runs is not a
witness, so the early return in _undoPlanOptimization now has a mutant
that names the smoke.
Issue: #500
User-Visible: no
`space/delete` (both runtimes), Optimize Undo and Import apply now treat
`asset-wait` like every reload path: nothing was adopted, so no toast, no
space switch, no history/undo reset — the dialog is released and the
scheduled reload owns the rest. Unit and smoke cover the refused branch for
all four paths; the spec's reactivity risk row states the real mechanism.
Issue: #500
User-Visible: no
`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
The residual 150–200 ms tasks are one space's wallBodiesGeometry — a
single polyclip union that cannot be split — not the aggregate the issue
is about. The threshold now names them and leaves room for a slower CI
runner; the mutant that computes everything in one task still produces
~1.5 s and fails.
Issue: #509
User-Visible: no
On the CI runner the smoke went red without any mutant: the observer was
started before the 60-room plan had finished drawing, and that render —
a long task of its own, unrelated to this issue — landed in the window.
The smoke now waits for a quiet main thread before it starts watching,
prints what it measured, and allows up to 450 ms per task: the residual
150–200 ms slices are one space's masonry union, which polyclip cannot
split, while the mutant that puts the whole aggregate back into one task
still produces ~1.5 s.
Issue: #509
User-Visible: no
Moving the aggregate out of render fixed the first frame, but the work
itself was still one uninterrupted ~1.5 s task on the large-house
fixture — the interface stayed frozen, just a moment later, which is the
same symptom the issue reports. cleanFloorAreaSteps yields after every
room; the runtime advances it with an 8 ms budget per frame and
reschedules until it finishes, so no slice outlives a frame and the
skeletons stay until the number is ready.
The smoke now watches longtask entries for the whole show, not only the
first frame: a single long task while the values are computed fails it.
Issue: #509
User-Visible: no
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
Re-acceptance after the version seam (#512 §7) from the full Validate
dispatch on 605991ef (run 34366858855): seven frames carried the version
text — three version-mismatch banners, three PDF footers, the support
preview — and now read `0.0.0-golden`; six of them were within threshold
and still showed stale betas (beta.4, beta.8). The other 162 frames are
kept as reviewed; 128 environment witnesses matched byte for byte.
Sub-threshold drift in nine unrelated frames (tray, resize handles,
compass, junction) is not accepted.
Issue: #512
User-Visible: no
Release: v1.74.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34366858855
Golden frames carried the card version text (about, version banner,
support/export previews), so every beta bump re-accepted up to 20
baselines that had not visually changed. The version now reaches the DOM
and stand requests through displayVersion() (src/card-version.ts); the
golden harness pins window.__HP_VERSION_OVERRIDE__ = '0.0.0-golden'
before the card is created. CARD_VERSION literals stay where the release
contract reads them; cache-busting and the console banner keep the literal.
Docs screenshots: `npm run docs:accept -- --identical` re-captures
locally, compares decoded RGBA pixels with the committed frames inside
Chromium (scripts/png-identical.mjs) and, only when every frame is
identical, refreshes the manifest fingerprints and captureScriptSha256;
bytes stay, any difference refuses with a per-frame count. First run on
this tree: 11/11 identical, manifest refreshed.
Mutants: version-seam-ignores-override, docs-identical-accepts-any-frame.
Issue: #512
User-Visible: no
The gate used to require every Validate run on the tag SHA to be green:
a cancelled duplicate or a red flake that a later re-run had fixed kept
the stable release blocked (v1.73.0, 09.09 — released by hand). Now the
verdict comes from the newest run that was not cancelled: not completed →
wait, success → pass, anything else → fail, no run → wait. The same rule
is documented for the perf workflow and the release runbook.
Mutants: release-gate-counts-cancelled-runs, release-gate-oldest-run-wins.
Issue: #511
User-Visible: no
The #160 contract test keeps the dense profile's longTasks block equal to
the historical isometric one, and both profiles boot through the same lazy
iso-scene-render chunk; the accepted split applies to both. Validate
34356856702 caught the divergence.
Issue: #507
User-Visible: no
Release: v1.73.0
Full Performance of the v1.73.0 stable candidate against the v1.72.0 product
(run 34354409872) was red on one check of the isometric profile:
longTask.countP95 16 → 20 against max(16×1.2, 16+3) = 19.2, with every
timing, longTask.totalP95Ms and longTask.maxSingleMs green. The trace behind
#506 shows why: since v1.73.0-beta.1 the isometric renderer is the lazy
iso-scene-render chunk (#160 Stage 3), so the single v1.72.0 boot task is
split in two around that import — the same work, +2 tasks.
Owner decision 2026-09-09: accept the split. countNoiseAllowance 3 → 5 for
large-house-isometric-v1 only; the ratio, the hard ceiling, total and
maximum single task keep gating real growth. The downloaded CI artefact
re-evaluated with this budget passes (limit 21, actual 20, no failures).
Documented in demo/performance/README.md; the budget test pins the
allowance and the untouched profiles.
Issue: #507
User-Visible: no
Release: v1.73.0
The beta.9 candidate failed the same phase twice in CI with "Resulting
promise was garbage collected" (runs 34346813552, 34347910231) while it
passes locally; a timer guard did not help, which points at a destroyed
context rather than a starved animation-frame chain. The wait now runs as
page.waitForFunction with raf polling, and the smoke logs frame navigations
and page crashes as `diagnostic …` lines so the next failure names its
cause. Verdict unchanged (animationName of the boot house, or 'missed').
Pre-release gate repair per PROCESS §11.4; locally OK ×2.
Issue: #506
User-Visible: no
Release: v1.73.0-beta.9
Validate 34346813552 on the beta.9 candidate failed only in browser smoke
shard 1: smoke_preloader phase 3 died with "Resulting promise was garbage
collected" — its rAF-only wait for the boot house had no other reference
while the runner withheld animation frames. A timer now bounds the wait
and keeps the promise reachable; the verdict is unchanged (animationName
of the house, or 'missed'). Pre-release gate repair per PROCESS §11.4:
locally `node demo/smoke_preloader.mjs` → OK ×2; all other heavy gates of
that run (golden, perf-smoke, backend, shards 2–3) were green.
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
Personally reviewed all 20 diffs from complete Linux capture34323743229: #505 header/control placement and beta version text only. 149 baselines remain untouched,112 exact witnesses. Product and timing guard unchanged; candidate reruns the same50ms search threshold after a single CI timing failure (local p95=17ms).
Issue: #505
User-Visible: no
Release: v1.73.0-beta.8
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34323743229
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
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