r1 M5: the runtime and field chunks release a card's frame and field cache
on disconnect (ledRelease from disconnectedCallback), never cache for a
disconnected card, and a chunk that lands after disconnect renders nothing
(the card's LED hook is connected-only). The profile now reports the three
caches of the shown space separately (shapes ≤ 50, visibility ≤ 50, retained
per-emitter fans ≤ 2500) through the runtime's ledStats, asserts 0 retained
entries and 0 live LED timers/frames/observers after every disconnect,
judges the Long Tasks of a 100-step camera series as well as the interaction
profile's camera scenario, runs one extra cold mount whose runtime response
is held while the card is removed, and enforces ≥ 7 samples after ≥ 1
warm-up — also on reports merged from parts (--warmup-only, --merge).
Issue: #780
User-Visible: no
r1 of the code review:
- M1: a strip keeps its marker's value badge, passive, at the half-length
anchor on the card and on the static card (no icon core, pulse or slot).
- M2: one room resolver for the strip's Glow — an explicit valid room_id of
the marker wins over the anchor room; a stale one falls back (stripRoom).
- M3: the chain remembers the space it is drawn in; a space switch finishes
it there, never in the space shown next, and opens no picker over it.
- M4: visibility is decided before import(): a hidden marker or an
HA-disabled device loads no LED chunk (ledVisible, also checks the stored
marker so a just-hidden one does not slip through a stale device list).
- M6: the static card is a full light_pools × live_states browser matrix.
Unit tests for M2/M3, smokes for M1/M3/M4/M6, five registered mutants.
Issue: #780
User-Visible: yes
r1 H1: a previous frontend sends every space without the unknown field and
the ordinary write path replaced the document with it, erasing every LED
strip. The shared ordinary-writer helper now copies the stored shapes of a
space the payload omits (config/set and Optimize); an explicit list from a
new client still deletes, a removed space takes its strips, and a link to a
marker the same write deleted unbinds by the usual rule. config/set reports
the normalisation counters after the preservation.
Issue: #780
User-Visible: no
Manual review requested after the automated reviewer stopped before a verdict.
Material 2c5b59aa: red, one High and six Medium findings.
Issue: #780
User-Visible: no
Accepted from the golden-images artifact of the full Validate on 63e751ad,
every frame reviewed side by side:
- new: led-strip-design-reference-off-light, led-strip-design-reference-on-light,
lighting-led-strip-glow-dark, led-strip-off-light, iso-led-strip-dark (the
designer's four strips on #868D94; compared with Led-On/Led-Off in
docs/design/led-strips/ACCEPTANCE.md);
- changed: geometry-devices-editor-dark, device-inbox-narrow-ru-dark,
device-dialog-desktop-en/de, toggle-entity-dialog-desktop-en/mobile-ru (the
«LED strip» tool after «Add»), geometry-decor-editor-dark,
furniture-categories-light, room-discard-dialog-mobile-ru,
support-phone-success-dark-en, support-phone-validation-light-ru — icons
the stale demo icon map lacked are drawn now.
demo/srv/assets/icons.js is the output of demo/gen_icons.mjs again (restores
63e751ad; the 4283cb13 trim is dropped so the frames match the accepted run).
Issue: #780
User-Visible: no
Release: v1.79.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36999711281
A full regeneration of demo/srv/assets/icons.js also picked up 29 icons other
tasks reference and dropped one, shifting unrelated golden frames. Keep the
committed map and add exactly mdi:led-strip-variant and mdi:link-variant(-off).
Issue: #780
User-Visible: no
The full Validate on a6c4001c found two things:
- smoke_help_affordance: the device dialog shifted when the LED tool chunk
arrived after it opened (the representation section appeared late). A
device without a strip now gets the static section at once (label in the
base dictionary); the press loads the tool, leaves the dialog through its
own guard and starts drawing for the same marker. A strip's device still
loads the tool with the dialog. The dialog alone no longer loads anything.
- smoke_lazy_admin_locale counted nine namespace chunks (stale since the
`tools` namespace); it now reads NAMESPACE_LOCALE_CHUNKS and accepts the
editor's own `tools-de`.
- demo/srv/assets/icons.js regenerated: the demo/golden ha-icon stub lacked
mdi:led-strip-variant and mdi:link-variant(-off).
Issue: #780
User-Visible: no
setServerConfig/setLayout/setMode/openMarkerDialog/close and the tray's own
«Device settings» + Delete + confirm instead of private card writes; config
writes pass through the fixture so revisions stay consistent.
Issue: #780
User-Visible: no
Stage 5 of #780.
- Import summary: «Strips left unbound after import: {n}» from the backend
`unbound_led_strips` count; «Optimize plans» reports strips passing
through walls per space and edits none (AC16).
- Linear field for long strips (ТЗ §13.2): pieces of at most the radius
along the polyline, emitters thinned to r/4, each piece clipped to the
visibility fans of its own emitters as separate clipPath children (no
boolean pass per piece), one floor clip for the whole layer, no fan at
all where nothing blocks within the radius; a grid index of body faces
and boxed inside tests; unchanged fields skip re-diffing. 50×50 on the
large house: first stable frame ~1.4 s, warm space ~1.1 s locally.
- led-strips-v1 profile: demo/benchmark_led_strips.mjs with the derived
large-house fixture (10×5, 50×50, none), absolute limits of the ТЗ table
in demo/performance/budgets-led-strips.json, exact counters (zero
recomputes on HA ticks/camera/colour, ≤50 cache entries, no growth over
20 cycles); added to the full performance workflow.
- Bundle: LAZY_LED_GZIP_CEILING 10 KiB, LAZY_LED_EDITOR_GZIP_CEILING 11 KiB
(measured + 10 %, rounded up); overlaps with the initial and editor
graphs refused; the lazy editor graph stays inside its ceiling.
- Smokes smoke_led_strip_draw/bind/glow, linked in smoke-links; 13 mutants
in the registry (7 browser guards in the inventory); config field registry
entry `spaces[].led_strips`.
- Golden: five new scenes on the `golden-led` space of the visual fixture
(`ledStrips` option, the designer's four strips on #868D94), matrix v71.
- Docs: LIGHT, DEVICE-PRESENTATION, USER-GUIDE (en/ru), UX-MODES,
ARCHITECTURE, ISOMETRIC, CONFIG-COMPATIBILITY, TOUCH-SUPPORT, demo/stand
README, performance README; docs/design/led-strips with the unchanged
designer archive, two paired frames and ACCEPTANCE.md; both changelogs.
Issue: #780
User-Visible: yes
hostRefs 4924 → 5050 (+126): the new lazy LED editor reads card state through
its own structural port `LedEditorHost` (src/led-strip-editor.ts) — not the
HouseplanEditorHostPort — the same way the stairs editor does. bundleBytes
2 607 854 → 2 664 132: the three new LED chunks (runtime, field, editor),
their ru/de/fr dictionaries and the shared geometry chunk. Re-measured at the
end of the task.
Issue: #780
User-Visible: no
Stage 4 of #780 (ТЗ §4–§5). «LED strip» next to «Add» draws a chain with
clean clicks only (pan, pinch, a second finger, cancel and the synthetic
click after navigation add nothing), snaps to the grid and to physical wall
faces / zero-wall axes, stops at the first face of masonry, partitions,
columns and windows (doors, gates and passages are cut by geometry), Shift
gives 45°. Ctrl+Z removes the chain's own point first, Esc finishes, a
double click or a click on the first point (≥3 vertices) closes; fewer than
two distinct points write nothing. A finished strip opens the device picker
(lights first, taken markers explained, «New device…» binds in that
dialog's own write, «Later» keeps unbound geometry).
A selected strip shows its vertex handles (a drag re-checks both neighbours
and the whole path) and a new Devices tray branch: device settings,
bind/change, unbind, show as icon, delete. The device dialog gets the
representation section: show as strip (restores a hidden shape at once or
draws one for this marker, behind the dialog's own save/discard guard),
show as icon, unbind/delete for a hidden shape. Every geometry or
representation change is one optimistic write and one LED command of the
device history; Undo/Redo restores only its own strip record and refuses
when a newer change sits on it. A rebinding renames the link in the marker
save, a deleted marker leaves an unbound strip, a bound marker may not move
to another space. Plan/Background show strips as a passive translucent mark.
The tool, its `led` dictionary (en static, ru/de/fr lazy) and placement
geometry are a new lazy `led-strip-editor` chunk (9.5 KB gzip), loaded only
on the tool, an editable strip in the shown space or a strip device's
dialog. The initial graph gets the loader and delegation only
(src/led-strip-card.ts); the lazy editor graph +122 B, inside its ceiling.
Mutant anchors follow the moved code (marker dialog guard, static LED layer).
Issue: #780
User-Visible: no
ТЗ §13.1: the static card with light_pools:false must not load the linear
field. led-strip-field.ts (2.1 KB gzip) is a dynamic import of the LED
runtime (5.0 KB gzip), loaded only when a strip is on in a Glow room with
a light scene, with the same fingerprint handshake and hashed-URL retry
(manifest role led-field, retry token counted).
Issue: #780
User-Visible: no
Stage 3c of #780 (ТЗ §7). In the volumetric View the stripe is raised like
a device tile: body lifted by ISO_TILE.lift (0.075 D), the edge swept
ISO_TILE.depth (0.1 D) below it in isoEdgeColor, an inert blurred floor
shadow from isoTileShadow for the current theme and floor; D takes the
shared ISO_ICON_SCALE. The field stays on the floor plane, the hit path
moves with the raised body, everything is in plan units so zoom cannot
detach it. Editors and the static card keep Flat.
Issue: #780
User-Visible: no
Stage 3b of #780 (ТЗ §8). houseplan-space-card draws a represented marker
as a passive strip through the same lazy runtime: no icon, no auto slot, no
round pool, no hit path, focus or actions. The stripe uses the drawn wall
geometry for the face offset, so light_pools:false builds no barriers,
visibility or timers; the linear field appears only with light_pools:true
and Glow; with live_states:false the stripe is neutral. Strip points vote
in the content frame. Probed in the demo: plain card — one passive stripe
with the source-colour core, no field, no hit path; light_pools — the field.
Issue: #780
User-Visible: no
Stage 3a of #780.
- src/led-strip-gate.ts is the only initial-graph foothold (ТЗ §13.1): which
markers a space shows as a strip (active and bound), the half-length
anchor that replaces their icon position, and one page-wide load of the
lazy led-strip-runtime chunk (fingerprint handshake, a hashed-URL retry on
the next explicit entry, never in a loop).
- The card: a represented marker takes no auto slot, draws no icon, casts no
round pool and is placed at the anchor; two call sites render the stripe
layer and the linear field from the chunk. The space model carries the
stored strips untouched; scaling, validation and geometry are in the chunk.
- src/led-strip-runtime.ts: the stripe is two strokes of one derived path
(outline #383838 t, core t/2, round joins and caps, t = 0.08/0.12 D),
white off, white core + field on with Glow, source-colour core without
Glow, grey dashed unavailable without a field. The hit path takes the
card's own device handlers (one action path) with radius max(22 px, t/2)
and the nearest stripe as owner. The linear field is the exact distance
field with the shared falloff: opaque grey bands of a luminance mask per
piece, pieces joined by lighten (the maximum), each piece clipped to what
its own emitters see, so a hidden part never lights through another part's
visibility; buried emitters emit nothing; a failed clip is dark. A bounded
per-space cache (50) counts geometry rebuilds.
- The initial View graph had 501 B of headroom. The furniture library copy
(furn.*, 104 keys × 4 languages, editor-only) moves into a new lazy `tools`
namespace (#627 mechanism) that the editor runtime awaits; the initial
View graph is 298 686 B gzip with the LED gate in it (−1 879 B vs the base).
Tests: test/led-strip-runtime.test.mjs (6: representation, gate anchor =
geometry anchor, falloff, states and radius, per-piece clipping and buried
strips, bounded cache), bundle and i18n fixtures for the LED chunk and the
fourth namespace.
Issue: #780
User-Visible: no
Stage 2 of #780. src/led-strip-geometry.ts (pure, not imported by the
initial graph): the anchor at half the polyline length; stripPieces and
visibleStripPath — a segment lying on a thick body face within
epsilonGeom is shifted t/2 into free floor, free floor and zero-wall axes
stay at 0, a face→floor transition is a connector without gap; emitter
samples epsilon outward on a face and none inside a body; placement that
stops at the first face and lets a strip touch and slide along it, a
vertex drag clamped on its path and both neighbours; the screen hit owner
with radius max(22 px, t/2) and a stable-id tie.
SpaceModel gains optional led_strips (render units, data only, no geometry
import in space-geometry.ts).
Tests: test/led-strip-geometry.test.mjs (10).
Issue: #780
User-Visible: no
Stage 1 of #780 — the data model. A space carries an optional
`led_strips: [{id, points, marker, active?}]` (custom_components/houseplan/
led_strips.py, pure, strict mypy).
- Type schema inside SPACE_SCHEMA: 2–50 finite numeric points (no strings,
booleans, NaN or off-canvas values), ≤50 strips per space including hidden
shapes, strict boolean `active`, marker = non-empty string or null.
- A config-level step after coordinate canonicalisation judges the shape
(two distinct points, non-zero length, a closed strip needs three distinct
vertices, a hidden shape needs a marker, unique ids per space) and the links
in the order the spec fixes: duplicates are rejected before any
normalisation (two links to the same missing id still conflict); a link to
a marker that is not live becomes an unbound strip (marker null, active
true, id and points kept), so a client that does not know strips can delete
a bound marker without its save failing; a live marker with an empty space
adopts the strip's space; a non-empty foreign space rejects the write.
- config/set answers with `led_strips: {unbound, space_adopted}` when the
write was normalised, so a new client re-reads; old clients ignore it.
- Space import remaps links through the marker id map; a skipped or
virtualised duplicate leaves the strip unbound; a coinciding old id never
binds. Plan-only export keeps geometry and nulls every link. Import details
report `unbound_led_strips`, computed by the server, never read from the file.
- Coordinates get JSON-noise cleanup only, like stairs (face contacts are
off-lattice), in both canonicalisers with a shared fixture case.
- Support package: counters only (total/unbound/hidden), no coordinates or ids.
Tests: tests_backend/test_led_strips.py (41, pure), test_ha_import_export
(6 cases: full round trip with a hidden shape, orphan count, remap against a
coinciding id, skip and virtual duplicates, plan-only), test_ha_websocket
(old client deletes a bound marker → save stands, counters, foreign space
rejects without a new revision). Full backend with the HA harness: 969 passed.
Mutating the shape check, the duplicate check, the orphan normalisation or the
space adoption each turns the pure suite red.
Issue: #780
User-Visible: no
Manual review requested by the owner after the automated reviewer failed.
Material e931a949: green, no High/Medium findings, one non-blocking Low.
Issue: #781
User-Visible: no
Manual review requested by the owner after the automated reviewer failed.
Material af09d36d: green, no High/Medium findings, one non-blocking Low.
Issue: #765
User-Visible: no
The body of _process.yml is read from dev (@dev, #623). After "Перейти на
ветку задачи" the working copy of job prepare is the task branch, and a
show/ship branch with a clean merge is not rebased before review: its
scripts/ may lag dev or be replaced. #749 fixed job integrate; prepare
still ran four control scripts from the material — the issue-body digest
for the anchor, --reuse of a green verdict (#499), validate-gate (#510)
and the spec-change check (#517). The material decided its own admission:
a branch whose review-doc-guard.mjs prints reuse=true merges without the
model. model_review took model-usage.mjs from the material too: a lagging
branch has none, and the publication silently wrote reason=missing.
Now prepare extracts one snapshot right after setup-node, before the
branch switch: `git rev-parse origin/dev` once and `git archive <sha>
scripts .github/workflows/validate.yml`, so the git fetch of the track and
rebase steps cannot mix versions. Every repo script of the job runs from
it via TOOLS — the track step and the rebase guard lose their own
extractions. The SHA goes out as job output tools_sha; the usage step of
model_review archives the same commit inside itself, so a snapshot failure
is a failure of the reporting step (continue-on-error), not of the stage.
The model session runs on that runner, so the usage line stays untrusted
input parsed strictly (#556, #737).
withMaterialAnchors is idempotent: a repeated call drops the separator the
previous call wrote instead of piling up `---` lines.
Tests: test/process-prepare-tools.test.mjs — the job contract (no step
calls scripts/ from the working copy, one pinned archive before the branch
switch, tools_sha reaches model_review) and the steps as they are, on real
bash and git: a branch behind dev without model-usage.mjs and with a
substituted review-doc-guard.mjs; the anchor digest, reuse and the spec
check come from dev, the usage line is data even after dev moved. Red on
the old workflow: all four. Harnesses of process-track, rebase-generated,
review-doc-guard and model-usage take the prepare snapshot. Three registry
mutants (reuse from the material, usage from moving dev, separators).
Canon: PROCESS.md §10.4 «Скрипты конвейера — из dev» covers prepare and
the usage step; «Расход модели» names it a pipeline step, not the reviewer's.
Issue: #765
User-Visible: no
After the rebase onto dev (#762) the raw dist total is 2 607 854 B against
the 2 605 172 B baseline, +2 682 B, over the 2 000 B band. The growth is the
task's own and deliberate: the floor-geometry key module and its call sites
(src/floor-geometry-key.ts, houseplan-card.ts, clean-floor.ts) plus the
wrapper of the new lazy space-editor chunk that keeps the initial View graph
under its absolute budget (initial View 300 572 B gzip, headroom 494 B).
No other metric moved past its band (portMembers 347 -> 349, band 5).
Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The floor-geometry key of #744 (+~143 B gzip) put the initial View graph at
301 097 B gzip, 31 B over the absolute 301 066 B wall in
scripts/bundle-budget.mjs. The comment over that wall says the next growth
is paid by moving code into lazy graphs, not by raising the budget.
`src/space-card.ts` imported its Lovelace GUI config editor
(`src/space-editor.ts`, the `houseplan-space-card-editor` element)
statically, so every View paid for a form only the dashboard editor opens.
`getConfigElement()` now imports it on demand, exactly as
`houseplan-card.getConfigElement()` already does for `./editor`; Home
Assistant awaits the returned promise. The editor's dependencies (lit, i18n,
space-geometry) stay in the shared chunk, so the new `space-editor-*.js`
chunk holds only the element itself (~1.1 KB gzip).
Measured: initial View 301 097 -> 300 489 B gzip (-608 B), headroom to the
301 066 B budget 577 B. The beta ceiling (300 142 B, band 2 000 B above it,
#699) and the budget are unchanged; lazy editor/onboarding graphs are not
touched.
Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
smoke_floor_geometry_cache failed in Validate run 36875756451 on one check,
ac2cPreviewWallStandsWhereAFreshCardDrawsIt, with every area check green.
The check read the moving wall only from the live layer (data-kind
"preview"). A host render during a held drag ends that layer: updated()
commits it, and nothing repaints it until the next accepted move. The
settled scene then draws the preview record itself, and its walls equal
those of a fresh card on that record. In the smoke, the "Room updated"
toast from the AC1 rename expires 3.5 s after the save, about when the drag
starts: locally the rename-to-drag gap is 3.47-3.6 s. On a faster runner
the expiry landed after the last move, so the check found no live path. A
light toggled in Home Assistant mid-drag gives the same red.
The drag now starts once the toast is gone, so the live frame normally
judges the live layer. If a host render still lands, the check judges the
settled union the same way; a stale union has none of the moved faces. A
new check takes the settled frame on purpose: a state change during the
held drag, then the union path and the areas must equal the fresh card's.
The live strips never pass through the floor-geometry caches, so the old
check stayed green with a floor key that ignores the preview. The new
check fails on that key, and a failure prints a diagnostic line naming the
judged layer and whether the settled union is the stored one.
Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The physical bodies, the wall union pool, the inner room contours and the
clean floor carried the global config epoch in their keys. Every edit of
any floor bumps it, so after one edit every other floor was cold again:
in large-house the first visit to an untouched floor rebuilt its wall
union and paid ~0.7 s flat / ~0.65 s 2.5D instead of ~40-55 ms.
A floor's geometry reads only its own config record (spaceModels) and
constants, so the key is now a content fingerprint of that record
(src/floor-geometry-key.ts), remembered per epoch and per record object.
The geometry also reads the current floor's config next to the model it
is given; when those records differ the key covers both. The live resize
preview is its own record, so preview frames get their own key; the
editor runtime seeds the pool and re-keys the bodies through the same
reader. The stairs editor no longer clears the clean floors of every
floor: the stairs are part of the floor's record.
The #735 switch-cycle guard now also sees the union pool and the inner
contours (optional members of the large-house card contract, so an older
comparison bundle reads 0). smoke_floor_geometry_cache proves the warm other floor and the
invalidation against an independent card (multi-floor push with shared
walls, a stair, a resize preview and its cancel); two mutants guard it.
Issue: #744
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Validate on the conveyor's rebase eb439e81 failed preflight: the branch
changes visual sources (stairs), so the screenshot check is strict on it,
and the fingerprint was stale against the moved dev (#739, #742, #759).
The branch is rebased onto b84465e5 (inventory counts merged with #742:
lifecycle 89, total 204/200) and `docs:accept --identical` re-captured
all 11 frames: pixel-identical, only the source fingerprint moves.
Issue: #740
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
A floor with stairs pays for them on every switch to it: the stair layer
is emptied on other floors, so Lit recreates every symbol on each return
and the browser lays out and paints it again. With 250 stairs (the
large-house fixture, the per-floor limit) that was 2,875 SVG elements and
about 40 ms per entry locally; each stair carried 3-7 separate tread lines
with four bound coordinates each.
The treads of one stair are now a single <path class="hp-stair-tread">
with one `M a L b` subpath per tread, in geometry order and with the
numbers the lines carried. Treads of one stair never overlap (straight:
parallel, >= 20 cm apart; spiral: inner ends >= 6.7 cm apart at the
3.6 cm stroke), so the path paints the same pixels at any opacity. The
outline points and the tread data are built once per cached geometry
object (cachedStairMarkup, weak keys), not on every render. The View and
plan-editor layers share the strings; outline, hit polygon, trapezoid,
arrow, attributes and handlers are unchanged. Floor 1 of the fixture
drops from 4,210 to 2,960 elements.
Witnesses: the unit test for the path data and its cache, and the
smoke_stairs markup checks in View and in the plan editor, are red on
dev. The stairs-view-tread-lines mutant restores the View lines.
Issue: #740
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd