# Changelog ## Unreleased (dev) ## v1.59.0-beta.10 — 2026-08-05 Tenth pre-release of the 1.59 line: one coherent device-state language and a focused set of wall, doorway-light and background-label refinements. - **One device-state language.** Marker plates and effects now come from one semantic resolver: yellow means actual work, orange means open/unlocked, unavailable is faded, and alarms are always red. The display list is reduced to Icon, Icon + activity and Value; the removed Ripple-only value migrates to Icon + activity on the next save. Activity distinguishes short events, persistent presence, mechanical travel and actual running, with reduced-motion fallbacks and no false event on first load or reconnect. Short effects also reset when a marker's effective source changes, while a cover's real opening/closing state takes precedence over the tap fallback. - **Clearer wall-drawing toolbar.** The Plan editor's former “Add” tool is now labelled “Walls”, and its new-wall thickness field sits immediately to the right of that button instead of after the rest of the toolbar. - **View-mode virtual walls sit behind thick wall bodies.** Their stored geometry still reaches the physical centreline, but in View the hatch masks the dash ends inside adjoining thick walls. All three editors keep saved dashes and live previews above the wall body so the complete span remains visible while editing. - **Door light respects thick-wall reveals.** Light now crosses a doorway only through the clear width of its physical wall tunnel. The near and far inner face spans jointly clip the spill, so the two jamb returns cast the expected cut-offs for an off-centre source; zero-thickness walls retain the previous doorway sector. - **Inline HA variables in decor text.** A label can now mix ordinary copy and multiple `{entity}` / `{entity:attribute}` references. Choosing a state or attribute inserts its token at the textarea caret. The separate unit field, one-slot hint, and preview are removed; old linked labels remain readable and migrate to inline tokens when edited. - **Canonical wall fragments.** Touching or overlapping virtual stretches on the same boundary and room pair now collapse into one `open_span`; Split pieces belonging to different room pairs remain separate. When removing the last virtual stretches leaves an original wall solid and uniformly thick, its atomic thickness entries collapse back to one whole-wall key. ## v1.59.0-beta.9 — 2026-08-05 Ninth pre-release of the 1.59 line: mixed virtual/thick resize integrity and clean, visible virtual T-junctions. - **Resize keeps mixed virtual/thick walls intact.** A live edge drag now moves `open_spans` and whole/atomic thickness keys together with the room polygons; commit no longer drops the thickness of the solid remainders, and Undo restores the complete transaction. - **Clean virtual T-junctions.** Real wall arms owned by different room contours receive the missing mitre at an open-span endpoint instead of a stepped corner. Virtual dashes and the two-click drawing preview paint above real wall bodies, so they stay visible right up to the junction centreline. ## v1.59.0-beta.8 — 2026-08-05 Eighth pre-release of the 1.59 line: beta.7 audit follow-ups, a fail-closed release path, full/static wall-render parity, and sunlight anchored to the room-side corners of thick window openings. - **Sun rays start at the inner window corners.** With wall thickness, the wedge's full source span now sits on the room-side face of the opening, so both crisp side edges begin at its two inner corners even at an oblique sun angle. The clean-floor contour still clips the light at the wall body. - **Beta.7 audit follow-up.** A Split crossing one `open_span` now keeps every valid room-pair piece and derives `open_to` for both children instead of silently turning one half back into a solid wall (`AUD-159B7-01`). - **Release safety.** `release.yml` resolves the published tag to its exact commit and waits for all matching Validate runs to finish green; missing, failed, cancelled or timed-out validation withholds the asset (`AUD-159B7-02`). The same fail-closed policy has automated negative tests. - **Test/render parity.** General-settings smoke includes the Walls group, inventory counts class-based and trail backend tests, and the static space card uses the same 3 px thin-wall hatch fallback as the full card (`AUD-159B7-03`, `AUD-159B7-04`). ## v1.59.0-beta.7 — 2026-08-05 Seventh pre-release of the 1.59 line: audit fixes for partial-wall geometry and lifecycle, plus wall fill colour under the hatch. - **Wall fill + hatch.** General setting `fill_colors.wall_fill` (default opaque white) paints under the diagonal hatch; the hatch stays. Thin-on-screen bodies keep the solid fill alone so the hatch does not collapse into noise. - **Atomic wall intervals (AUD-159B6-01).** Thickness and open cuts follow shared-boundary / open-span endpoints, not whole polygon edges — a partial shared stretch no longer leaks thickness onto the outer remainder, and an open span away from the edge midpoint clears only its own piece. - **Open-span geometry transaction (AUD-159B6-02).** `open_spans` ride in the resize snapshot/Undo; Split / Merge / Delete rekey, clip and sync `open_to` in one step without resurrecting a legacy stretch mid-mutation. - **Backend `open_spans` schema (AUD-159B6-03)** with cap / finite points / dedupe; frontend fail-soft sanitises malformed entries. - **Warm owner vs pending nav (AUD-159B6-04):** adopting a warm viewport clears the global LS pending mode so a neighbour card cannot overwrite the owner's editor and draft. - **Smoke / inventory hygiene (AUD-159B6-06):** two-click openwall fixtures, island-by-id, render-perf hook on `_openPairs` / `_buildModel`, inventory counts `test` and `it`. ## v1.59.0-beta.6 — 2026-08-05 Sixth pre-release of the 1.59 line: partial open-wall spans and wall-centric Delete. - **Partial open (virtual) wall spans** (`space.open_spans`): two-click openwall (anchor → second point on a shared wall, clamped to nearest corners); crosshair cursor; openings on virtual stretches are removed and cannot be placed; thickness clears on open and restores from neighbour / 15 cm on close. Legacy `open_to` still expands to the full shared boundary on read. - **Wall-centric Delete**: virtual → solid; shared solid → confirm merge (whole shared boundary); outer wall or click inside room → confirm delete room. ## v1.59.0-beta.5 — 2026-08-05 Fifth pre-release of the 1.59 line: wall-thickness redesign (seamless ±½ rings, inner floor/area/sun), draw-with-thickness, new-space opens the plan editor, and audit hygiene from the beta.4 recheck. - **Draw with wall thickness.** The Plan Draw toolbar has a thickness field (default 15 cm / HA inches). New rooms get that thickness on commit; shared walls keep the neighbour's value; empty clears. Live thick preview while drawing. - **New space opens the plan editor.** Saving a newly created space switches to Plan with the draw tool armed, so an empty floor is not left in View with nothing to look at (same as the first-space onboard path). - **Wall thickness redesign (docs/WALL-THICKNESS.md).** Walls grow ±½ from the centreline (outer and shared); bodies are outset−inset rings unioned into one hatch (seamless L/T as in the reference plan); fills/glow/sun clip to the inner contour; displayed m² is the clean floor; sun wedges narrow through the opening tunnel. Resize/undo/scale keep and re-key `walls`; openings resolve by wall angle; plan-editor mode restores after `can_write`. Hatch colour matches the wall outline. ## v1.59.0-beta.4 — 2026-08-05 Fourth pre-release of the 1.59 line: wall thickness in the plan editor, and a fix so editors keep a white drawing sheet under the grid even when a backdrop image is loaded. - **Wall thickness in the plan editor (docs/WALL-THICKNESS.md).** A wall carries one thickness (`space.walls: [{ key, cm }]`), entered in cm or inches from HA's unit system. Shared walls grow half into each room; outer walls grow inward; open boundaries refuse thickness. Hatched bodies follow `show_borders` (always visible in the plan editor); openings cut the slab full-depth and door swings start at the inner face. Area, glow and sun stay on the polygon. The static space card draws the same bodies. Unit + smoke: `test/wall-thickness.test.mjs`, `demo/smoke_wall_thickness.mjs`. - **Editors keep a white sheet under the grid with a backdrop.** With an uploaded plan image, the plan / devices / decor editors used the theme card colour under the grid instead of the white drawing sheet of a hand-drawn plan. They now stay white; View is unchanged (theme under an image, white without one). ## v1.59.0-beta.3 — 2026-08-05 The third pre-release of the 1.59 line: you can furnish the plan with top-view symbols at real size, hide the decor layer or the openings from View without losing them, aim card-mod at stable `data-*` hooks, and let Home Assistant format entity values the way its own more-info does. A handful of editor polish lands with it — bead-sized corner handles, the backdrop editor opening on its own tool, virtual walls following the room-border switch — plus the audit follow-ups (write policy, README differentiation, tighter validation). Wall thickness is approved as a written spec only (docs/WALL-THICKNESS.md); it is not coded yet. - **Two new switches in a space's settings: «Hide the decorative layer» and «Hide doors and windows».** Both only hide. The shapes, labels, furniture and openings stay in your config, and each stays visible in the editor that owns it — the backdrop editor always draws decor, the plan editor always draws openings — because a layer you cannot see is a layer you cannot edit. What an opening *means* is untouched either way: light still spills through it, the sun still comes in at a window, a contact sensor still opens it; only the symbol is gone. Both are off by default and store nothing when off, so every existing plan reads back unchanged. - **A space that does not draw room borders no longer draws virtual walls.** With «Always show room borders» off, the dashed stretches of an open boundary used to survive on their own, leaving a plan with no walls except a few floating dashes. They now follow the same switch. The plan editor still shows them — the Open-boundary tool has to show what it edits. - **The backdrop editor opens on the tool it is named after.** With a picture in the space, «Backdrop image» is armed the moment the editor opens, so dragging the picture works straight away instead of after finding the tool. The frame was already drawn on open, which promised a draggable picture; the promise is now kept. Select is still one click away and still leaves the picture's body to the one-finger pan. - **Corner handles are beads again, not blobs.** Every corner handle in the card — the backdrop frame, the room-resize frame and the robot-map fit — paints a circle a **quarter** of its old radius while its clickable area is unchanged, matching the text block's handles. A handle the size of a room hides the thing it is there to adjust. - **You can furnish the plan (docs/FURNITURE.md).** The background editor gains a seventh tool, **Furniture**: a grouped palette of ~30 top-view symbols — furniture, appliances, plumbing and a few odds like stairs and a rug — that are placed at their **real size**. Pick a sofa, correct the 2.2 × 0.9 m in the two fields if your sofa is a different one, click the plan, and 2.2 m of *your* plan is what it takes, because the size goes through the space's `cell_cm` like every other length in this card. While you place or drag it, the nearest wall within ~30 cm claims it: the piece's back lands flat on that wall and turns to it, so "put the bed against that wall" is one click. Shift suspends the magnet, as Shift suspends every snap here. A placed piece is selected in the **Select** tool and wears the text block's frame — the corner handles now set **width and depth independently** (a bed is not made deeper by being made wider), with live badges showing both in metres or feet, and the handle above it turns the piece in 5° steps. The symbols are **drawn by us, in code**: an icon set draws a sofa seen from the front in a 24 × 24 square, and stretching that into a 2.2 × 0.9 m rectangle gives an icon lying on the floor rather than a plan. That also settles the licence question outright — nothing third-party ships in the bundle. Every piece carries `data-kind="furniture"` and `data-symbol`, so card-mod can colour all the plumbing in one rule. New kind, nothing migrated: a plan written before this reads back byte-for-byte. - **The plan has stable hooks for card-mod (docs/STYLING-HOOKS.md).** Every object the plan draws now carries the same identity — `data-hp` says what it is (`device`, `room`, `room-label`, `opening`, `decor`, `space-tab`), `data-id` is its id in your config, and where it applies `data-entity`, `data-area` and `data-kind` (door/window, line/rect/ellipse/text) come with it. That is all this is: we do not ship a CSS field, do not add a theme editor and do not support user stylesheets — we simply stop getting in the way of a power user who has already installed card-mod and only needed something stable to aim at. The names are a contract now: renaming one is a breaking change. Everything NOT in that table — editor chrome, boot classes, layer wrappers, dialog markup — is explicitly not the contract. The doc carries the table, three worked examples, the shadow-DOM limits (you can style an `ha-icon` host, never its insides) and a plain disclaimer that card-mod is not ours to support. - **Values are formatted by Home Assistant, not by us.** Wherever the card prints one entity's state — the value badge of a «value instead of icon» marker, a live decor label, the device info card — it now goes through `hass.formatEntityState` (and `hass.formatEntityAttributeValue` for an attribute), the same call HA's own more-info makes. So a sensor's `display_precision` is honoured, the decimal separator is the one your locale uses, and `on` finally reads as *Включено* instead of `on`. The unit lands exactly once — the formatter usually appends it, and where it does not we still do — and your own unit on a decor label replaces the entity's rather than piling on after it. An older Home Assistant without those methods behaves exactly as before. The small °/% plates next to an icon keep their own compact form on purpose: they are a derived reading (an average over the area's sensors, a climate device's `current_temperature`), not an entity state, and the plan reads as one instrument panel because they all look alike. - **The text block's handles are a quarter of the size** (owner's request). The corner circles and the rotate handle above a selected label are now ink the size of a bead instead of a button, so the frame stops covering the very words it frames. The area you can grab is unchanged — an invisible finger-sized circle still owns the gesture at the old 1.8 % of the visible view, exactly the split the wall-resize handles use. - **Wall thickness — the approved spec (docs/WALL-THICKNESS.md).** Not implemented yet; the document captures the model (thickness on a wall, not a room edge), segment keys that survive resize, hatching that does not affect area, and how openings render in a thick wall. Audit follow-ups from `docs/AUDIT-RECOMMENDATIONS.md` (2026-08-05): - **Write policy aligned (P0-4).** `admin_only` now defaults to **on** for new installs and when the option key is missing; `houseplan/config/get` returns `can_write` from `may_write`, and the card's editor chrome follows that flag. Missing `hass.user` no longer fails open into the editors. - **README differentiation (P0-3).** Comparison table now positions House Plan against both YAML/SVG incumbents and GUI draw cards (e.g. easy-floorplan): shared `.storage` map + area-bound rooms, not furniture CAD. - **Validation tighten (P3-4).** Marker `binding` must be `device:…` / `entity:…` / `virtual`; `ripple_color` is `#rrggbb`; decor rect/ellipse extents are strictly positive; space `id` matches `SPACE_ID_RE`. - **Hygiene (P3-5).** `quality_scale.yaml` points at the real config-flow test file; card-level `tap_action` documented as deprecated/ignored. ## v1.59.0-beta.2 — 2026-08-04 The second pre-release of the 1.59 line, and it is about the words you put on the plan. A decor label can now read a live value off an entity, so a caption becomes a readout without becoming a template language. The text block itself lost the choice between three font sizes: it is scaled by its corners and turned by its handle, like every other object on the plan, and it may have more than one line. Under that, the three findings of the beta.1 audit are closed — two identical cards no longer share one warm memo, a rapid double re-mount no longer eats the draft, and an expired dialog no longer holds a plan file in memory. - **A decor label can show a live value (docs/LIVE-TEXT.md).** A text shape gains three optional fields — `entity`, `attr`, `unit` — and its `text` becomes a template whose `{}` is where the value lands: `Бак {}` reads *Бак 68 %*. Without a placeholder the value is appended; without an entity the label is byte-for-byte the static one it always was. The unit comes from the entity unless you type your own; a dead, unknown or missing entity shows an em dash instead of quietly vanishing. Nothing is rounded or reformatted — the value is what Home Assistant reports, because rounding belongs to the sensor's `display_precision`. Not a template language: one value, one place, no syntax to get wrong. - **The text dialog gained an entity picker, an attribute picker and a live preview.** The attribute list is the chosen entity's own attributes, the unit field shows the entity's unit as its placeholder, and the preview is rendered through the very same substitution the plan uses. - **A text block is sized by its corners and turned by its handle.** The choice between three font sizes is gone: select a label and pull a corner to scale it, or use the handle above it to rotate in 5° steps (Shift for any angle) — the same mechanics as the backdrop frame. Labels drawn with the old Small/Medium/Large come back at exactly their old size, and the first drag replaces that setting with the scale it meant. - **A label can have more than one line.** The text field is a textarea now: your line breaks are stored and drawn as line breaks, centred, with the block growing around its anchor. Nothing wraps by itself — a caption that reflows on a state change is a caption that jumps around the plan. - **The text tool edits the label you press on.** Drawing tools own the canvas (a new line must be able to start on the end of an old one), and that stays true — with one exception: pressing an existing label with the text tool opens ITS form instead of starting a second label on top of it. Empty canvas and non-text shapes still create a new label. - **Two identical cards on one page no longer share a warm memo (AUD-159B1-01).** The memo key was the window size and the card config, which cannot tell two placements of the same config apart: the newer of them was the last writer, and a card re-created in the OTHER placement woke up with its neighbour's floor, editor mode and zoom — while the draft of its real predecessor was eaten by the mode guard. The memo now keeps one entry per card PLACEMENT: `location.pathname` (the dashboard view) joins the key, and inside a key the entry is claimed by DOM slot — the parent element and the index in it — with a live neighbour's entry never adoptable. When two placements are genuinely indistinguishable only the settled header height is adopted (it is the same for both), never the viewport and never the dialog. - **A rapid double re-mount no longer destroys the draft (AUD-159B1-02).** `disconnectedCallback()` cleared the «I still owe my predecessor a dialog» flag BEFORE taking its snapshot, so a middle instance in an A→B→C rebuild wrote `dlg: null` over a draft it had not yet restored, and the third instance got nothing. The snapshot now runs while the flag is still set: an unsaved dialog simply travels down the chain until one instance lives long enough to reopen it. - **An expired dialog no longer holds its payload (AUD-159B1-03).** The 10-second revive TTL was only a rule checked at revive time; the entry itself kept the dialog — for a space dialog that is a whole plan file as base64 — until the page reloaded. Detaching now arms a guarded eviction that frees the payload the moment it stops being revivable (and drops the stale slot, so the next claim is unambiguous again). ## v1.59.0-beta.1 — 2026-08-04 Minor pre-release: the card survives a Lovelace re-mount bit-for-bit — your pan, your zoom and even the dialog you had open come back with it — sun rays get a hairline edge so they read on white paper, the «+» that adds a space leaves the Plan editor for the tab row, and the list of plans already uploaded to the server stops collapsing into a stripe. - **The card no longer twitches — and no longer loses your open dialog — when you come back to the tab (docs/WARM-REMOUNT.md).** Lovelace re-creates the card element on a websocket reconnect; v1.58.0 removed the preloader flash, but the new instance still had to guess what the dead one had been looking at. Measured: the PAN never left the instance (a view parked in a corner came back re-centred: x=50 → x=250 at zoom 2.2), and the EDITOR zoom is deliberately not persisted while the editor MODE is — so a re-mount inside an editor came back at 1.0 instead of 3.0. The warm memo now carries the whole viewport (space, mode, zoom, the `_view` rect itself, the view-mode snapshot, the «show far objects» frame, the selected tool and selection, the local «show hidden» toggle), so the restore is the same RECTANGLE, not the same zoom number — bit-for-bit, verified frame by frame. - **An open dialog survives the re-creation with its draft.** The memo is module state and is never serialised, so the live draft object — a half-filled device dialog with its uploaded PDFs included — moves over for free. The rule is «revive the draft, never revive the decision»: the confirmations for «Align everything to the grid» and for a room merge are deliberately NOT restored (a modal whose whole content is «press OK to rewrite your plan» must not greet a returning user), nor is a tap confirmation (it closes over the dead instance), nor the floor import wizard (it reopens itself), nor any dialog with a save in flight. Revival requires the same space and the same mode, happens at most once (the snapshot is consumed), and only if the previous instance died within the last 10 s. A dialog closed on purpose — Esc, Cancel or Save — writes `null` into the memo on the very next render, so it can never come back (smoke_warm_dialogs). - **Sun rays get an edge (docs/SUN.md, «The rim»).** On a white plan a wedge of light was nearly invisible, and no amount of opacity could fix it: painting light means adding luminance, and white paper has none left to give. Every lit wedge now carries a 1 px black hairline along its two SIDE edges — the ones running inward from the ends of the window. It fades to nothing on exactly the same axis, the same curve and the same 85 % threshold as the fill (a second gradient built on the fill's own stops), stays one screen pixel at any zoom (`non-scaling-stroke`), is cut by the room like the wedge itself, and lives in the same layer — so the 3° threshold, the two-second fade, cloud cover, night and the editors govern it without a line of extra logic. Peak opacity 0.42, picked against a white sheet and the dark glow canvas alike. The «shade instead of light» model of docs/SUN-CONTRAST.md was rejected in favour of this; that file now records the decision and keeps the analysis behind it. - **The «+» that adds a space is not an editor tool.** The button next to the floor names only existed inside the Plan editor, so adding a second floor meant first opening an editor you did not want. Adding a space is navigation, not markup: the «+» now sits in the tab row in every mode — View and all three editors — exactly where the per-space gear already lives, and under the same admin rule. The kiosk is a shop window: the button is not rendered there at all (its header is `display:none`, and a hidden-but-present node is still clickable from script). The tab row wraps as before at 390 px (smoke_gear_tabs measures the overflow). - **«Already uploaded» shows the plans again.** In both space dialogs (new space and space settings) the list of plans stored on the server collapsed into a thin rounded stripe: the rows were rendered, the box itself was 14 px tall. A scrolling box is a flex item whose automatic minimum size is zero, and the dialog body is a flex column with a 66 vh cap — so the picker was the one child that could be squeezed to nothing. It no longer shrinks and keeps a floor of its own; up to five thumbnails are visible at once and the rest scroll. The empty and loading states stay readable instead of clipping their own text (smoke_plan_picker measures the heights — the old smoke only counted DOM nodes and passed). ## v1.58.0 — 2026-08-04 Minor release: the backdrop picture becomes a movable, scalable object, the opaque plan sheet is redefined as the room contours, the decor drawing tools stop stealing clicks, and «Align everything to the grid» tells the truth about what it is going to do. ### The backdrop picture can be moved and scaled (docs/BACKDROP.md) - **The plan image is no longer nailed down.** In the Background editor the picture gets a transform frame: drag it by its body to move it, pull a corner to resize it evenly (proportions kept, the opposite corner stays put). A live badge states the picture's real size in metres (or feet) while you drag, through the space's `cell_cm`. Position and size land on the grid; Shift steps off it. Nothing else moves — rooms, doors, devices and decor stay put — and there is no rotation. - Three new optional space fields: `plan_x`, `plan_y`, `plan_scale`. Their absence is exactly the old behaviour, so **existing plans render bit-identically and nothing is migrated**. «Вернуть картинку» in the toolbar clears them. - **The opaque plan sheet is now always the room contours.** It used to be the image rectangle whenever a picture was attached; now the picture is drawn ON the sheet — above it, below the walls. The scene colour (`bg_color`, the `daynight` sky) therefore reaches the exterior walls on an image plan too. Consequence, deliberate: a transparent picture over a space with no rooms drawn shows the scene background through itself. - «Вписать всё» counts the moved and scaled picture, so it can no longer be left off screen. ### Drawing tools no longer grab the shape under the cursor - **A drawing tool owns the canvas.** With Line, Rectangle, Oval or Text picked in the decor editor, clicking on an existing figure now starts a NEW figure instead of selecting the old one — so a line can begin exactly on the end of another line, and a rectangle can be drawn on top of a filled one. Double-clicking a text under a drawing tool no longer opens it for editing either. The same inertness applies in the «Картинка-подложка» tool, where a decor shape lying over the plan must not block the picture's own drag. - «Выбрать» and «Стереть» are unchanged: shapes stay clickable there, which is where selecting and deleting belong. ### «Align everything to the grid» keeps its promise (AUD-158B1-01) The action has no undo, so its confirmation dialog is the only safety there is — and it was understating the damage. - **The maximum shift is measured on the geometry that is actually written**, not on an intermediate one. A rect was measured by its origin and its far corner only — so the minimum-size correction that widens a too-thin box afterwards went unmeasured, and the two corners that carry the X error of one side together with the Y error of the other were never looked at at all. An ordinary box could move √2 times further than promised; a box thinner than one grid step, much further. - **Each space is converted through its own `cell_cm`.** The dialog used to take one normalised maximum and multiply it by the cell size of the FIRST space: a plan whose ground floor is drawn at 5 cm per cell and whose attic is at 100 cm promised 2.5 cm for a vertex that moved 50 cm. The report now carries the maximum in centimetres and the space it belongs to, and the dialog names that space when there is more than one. - The last tenth of a centimetre is rounded **up**: the promise can never be smaller than the deed. ### An opening whose only error is its angle can now be fixed (AUD-158B1-02) - A window sitting exactly on its wall but holding a wrong `angle` used to come back as `changed: false` — the dialog said there was nothing to move and offered no button, while the returned plan differed from the one given. Such openings could never be aligned. The angle is part of the diff now; the dialog counts them separately, and the displacement is measured on the opening's ENDS, so turning it in place is not free in the report either. Turning an opening end over end (180°) is still counted as a correction but displaces nothing. ## v1.57.0 — 2026-08-04 Minor release: the canvas becomes infinite — no more "plan size", no more edge to run past — plus a tap action for curtains and blinds, a polish pass over the sun, live rulers while placing openings, and a warm re-mount that no longer flashes. ### The infinite canvas (docs/CANVAS.md) The idea of a "grid size" or "plan size" is gone. The canvas is conceptually unbounded — any coordinate is legal, with a `+/-5000` limit kept only as garbage insurance (about 60 km of plan at the card's own scale). - **If your plan ran off the edge, it now just works.** Some plans grew past the old square and devices simply could not be placed outside it; the only workaround was to redraw everything. That limit no longer exists: draw, drag and place devices anywhere, and nothing needs to be redrawn. Your stored plan is untouched — the coordinates mean exactly what they always meant, and there is no migration. - **Existing plans open exactly as before.** Verified on a real three-floor config: the opening frames come out bit-for-bit identical to v1.56.0. - **The opening view is derived from what is actually drawn.** `view_box` is now an optional hint for the very first frame only; after that the starting view is always computed from the real content, with outliers rejected so one stray room cannot shrink the whole plan. - **Pan at any zoom, in every editor.** Dragging the plan used to work only above 100% zoom; now it works at any zoom and in all editing modes, with one screen of slack around the content. - **Zoom out to 3x the content**, so you can see the whole plan and the space around it. - **"Fit everything" button** — one tap frames all the content again. - **A "home is that way" arrow** appears when you have panned away from the plan, pointing back to it. - **An adaptive grid.** The grid picks its step from the zoom (1 / 2 / 5 / 10 ... 1000) instead of a fixed cell, and the dots stay a muted hint rather than a pattern that competes with the plan. ### Curtains and blinds - **New "Open/close" tap action for covers.** Closed opens, open closes, and a cover in motion stops. Offered only for the unprotected device classes — garage doors, doors and gates stay on the info dialog, on purpose. - **The icon itself tells you the state**: it morphs between the open and closed shape (a table of cover classes, plus aliases resolved from the base icon), and pulses with a soft ring while the cover is travelling. - **A curtain never wears a coloured plate.** No state fill behind a cover marker, ever. - **The cover is found among all of the marker's entities**, not just the primary one — which is the common case for Aqara and zigbee2mqtt devices — and an explicit "Open/close" choice always wins the indication. ### Sun polish - **Brighter, shorter rays.** Opacity up to 0.30, length down 30%, with crisp sides again. - **Light fades only along the ray**, and is guaranteed to reach zero before the wedge ends — the wedge is now clipped by distance along the ray, so the "bright rim" that appeared with a low, oblique sun is gone. - **A hard 3 degree threshold.** The rays appear and disappear at 3 degrees of solar elevation over a two-second fade, instead of creeping in. - **The day/night sky catches up with the sun** when you come back to the tab: a gap of more than 3 degrees is repainted at once, while normal movement keeps breathing smoothly. ### Everything else - **Live rulers and the centre magnet while PLACING an opening**, not only while dragging one — with the distances measured along the owning room's own edge. - **A warm re-mount.** When Lovelace recreates the card on reconnect there is no preloader and no flash of the plan any more, and a dropped WebSocket never blanks a plan that is already on screen. - **Room borders and names default to dark grey `#55606c`** instead of the accent blue. If you picked a colour explicitly, nothing changes. - **An "About" block at the end of the general settings** — card version, GitHub and Telegram links. - **The icon angle now steps by 5 degrees.** - Audit fixes: the sun-ray cache survives local edits, hidden devices no longer stretch the frame, the editor's grown frame does not leak into view mode, an outlier room no longer inflates icon sizes, and a kiosk pan stays a pan instead of flipping to the next floor. ## v1.56.0 — 2026-08-03 Minor release: the sun comes to the floor plan — a compass, a day/night backdrop and real sunlight through the windows — plus room temperature from climate devices, a design-token pass over the UI, live rulers for opening drags, and a sun-cache fix. - **The sun on your floor plan** (docs/SUN.md). Tell the plan where north is — a compass dial in the general settings, with a per-space override — and the card starts living by Home Assistant's own `sun.sun`: - *Day/night backdrop* (`bg_mode: daynight`): the scene behind the plan follows the sun's elevation — white at full day, warm gold through the golden hour, cooling dusk, deep night. The plan itself dims only ~10% at night, so the rooms stay readable. - *Light through the windows* (`sun_rays`): every window on an exterior wall casts a soft wedge of light into its room — clipped by the room's own contour, warmer near the horizon, stretching long at sunrise and sunset, short at noon, gone after dark. - *Clouds, if you want them* (`weather_entity`): point at a weather entity and an overcast sky mutes the wedges; rain or snow puts them out. A dead sensor never kills the sun. - The plan now sits on an *opaque sheet of paper* traced along the room contours: the scene background — `bg_color` or the daynight sky — is visible only around the walls and never bleeds through the rooms; in day/night mode the sheet casts a soft shadow. - The whole feature is silent until the compass is set; without `sun.sun` the settings dialog explains why. - **Room temperature from climate devices.** A new checkbox in the device dialog puts the AC's or thermostat's `current_temperature` on the badge next to its icon and into the room's average temperature. - **UI modernization.** A design-token pass over `styles.ts` — 209 hard-coded values unified into spacing/radius/font/shadow scales — and native `ha-switch` / `ha-slider` controls in the dialogs, with a hard fallback to the old plain inputs where HA components are unavailable. - **Live rulers for opening drags.** Dragging a door or window now shows the live distances to both ends of its wall, measured along the owning room's edge; a dashed guide and a soft magnet snap the opening to the wall's center (hold Shift to disable the magnet). - **Fix DEV-B701-01** — the sun-ray cache is now invalidated by local geometry edits too, so a wedge can no longer go stale after you move a wall locally before the server confirms the change. ## v1.55.3 — 2026-08-02 Patch release: two fixes for the v1.55.2 first-open veil, found by an adversarial lifecycle audit (AUD-1552-01/02). - **A dashboard rebuild during the first open can no longer leave the plan hidden forever** (AUD-1552-01). Disconnecting the card while the boot veil was up (Lovelace recreates its DOM, a view switch remounts the card) killed the settle timers but kept their ids, so nothing ever lifted the veil again. The veil lifecycle now restarts from every reconnect — with a fresh clock and an unconditional hard cap — and the fade-out also survives a mid-fade remount. - **The veil no longer opens early, right before a late panel lands** (AUD-1552-02). Two equal height reads at 200/400 ms used to reveal the plan at ~400 ms, so Home Assistant chrome arriving at 450+ ms jumped on a visible plan — exactly what the veil was meant to prevent. The veil now holds for a full protective window with trailing quiescence (height changes near the cap extend the wait), and for a short grace after the reveal any later shift glides via a height transition instead of snapping. Deliberate height changes (entering an editor) still apply instantly. ## v1.55.2 — 2026-08-02 Patch release: a calmer first open — no zoom flash and no layout jumps while Home Assistant is still settling — plus a new background color setting and two bits of editor polish. - **Opening a plan no longer flashes the default view before your saved zoom** (HP-1551). The saved zoom used to be applied a beat after the first render, so the card painted the default fit for a frame and then snapped to your position. It is now applied synchronously, before the first paint — the plan appears exactly where you left it. - **A first-open veil hides the plan until the layout settles** (HP-1552). While Home Assistant is still loading its side panels, the stage height can change several times, and the plan visibly jumped along with it. The card now waits under a dark veil with a pulsing house outline until the height stops moving, then reveals the plan already in place. Kiosk mode skips the veil entirely. - **New setting: background color around the plan** (HP-1554). The area around the plan can now be any color — set it in general settings for the whole card, or override it per space. The backend validates the value as a #rrggbb hex color. - **Round caps and joins on background-editor lines** (HP-1553). Strokes used to end and meet in square cuts; line ends and corners are rounded now, so traced walls and outlines look clean. - **The Resize tool's wall handles now look the part.** They are half the previous size, show a wall-with-arrows glyph that rotates to follow the wall, and use the grab cursor — while the hit area stays exactly as large as before. ## v1.55.1 — 2026-08-01 Patch release: the four findings of the v1.55.0 audit, all in the new Room resize tool (HP-1550-01..04). - **A pending save can no longer leak a live drag to the server** (HP-1550-01). The resize preview used to be written straight into the shared config object, and a debounced save still queued from a previous edit could snapshot it mid-drag — so a resize you then cancelled with Esc quietly survived on the server and came back after a reload. The live geometry now stays in a separate overlay that only the renderer sees; it reaches the config exactly once, when the handle is released. - **An interrupted drag is cancelled, not committed** (HP-1550-03). When the system cuts a drag short — switching apps, palm rejection on a tablet — the tool used to treat it as a release and save the half-finished geometry. Such interruptions now take the same path as Esc: the original geometry comes back, no undo step, no save. - **A door in the middle of a wall no longer blocks resizing it** (HP-1550-04). The door's invisible hit area used to sit on top of the wall handle, making such a wall ungrabbable for both rooms. In the resize tool the handles now own the hit test (openings are not editable there anyway — they simply travel with the wall), while all other editor tools keep openings clickable exactly as before. - **The ~30 cm minimum size now holds for any room shape** (HP-1550-02). The old check only measured parallel opposite walls, so a triangular room could be squeezed into a sliver, and a rotated room could scale its real short side below the floor unnoticed. Both measures are orientation-independent now: wall drags respect every obstacle in the wall's path (a triangle's apex, a slanted wall), and the scale frame uses the room's true minimum width. Rooms already thinner than the floor may still be improved, never made worse. ## v1.55.0 — 2026-08-01 Minor release: rooms are no longer set in stone — a dedicated Resize tool in the plan editor changes their size by dragging walls; plus the two findings of the v1.54.3 audit (HP-1543-01/02). - **Room resize — a new «Размер» tool in the plan editor** (spec: docs/RESIZE.md). Every wall gets a handle at its midpoint; drag it and the whole wall moves, staying parallel to itself, with grid snap. Works for polygons (L-shapes included) and for legacy rectangles alike. - **Shared walls always move together.** If the dragged wall coincides with a neighbour's boundary, the neighbour follows: your room grows, the neighbour shrinks — gaps and overlaps cannot appear by construction. At T-junctions only the coinciding stretch follows, inserting new corners into the neighbour's outline where needed. - **Live numbers while you drag.** The dragged wall and its two adjacent walls show their lengths in real meters/feet, and the room area in m² updates live at the room centre — for a shared wall, the areas of both rooms. - **The wall stops where it must.** Rooms cannot get thinner than ~30 cm (neither yours nor the neighbour's), an outline never crosses itself, a growing wall stops at foreign rooms and island rooms, and doors and windows are anchors: an opening travels with its wall, and a wall carrying openings can never get too short for them. - **A scale frame for the whole room.** Click a room in the resize tool to select it: dragging a corner of the dashed frame scales the outline proportionally, with the same stops applied. - **Esc and Ctrl+Z.** Esc cancels the current drag and puts the original geometry back; one released drag is one undo step, and Ctrl+Z walks back up to 30 of them while the tool is active. - **Fix: room overlap detection missed equal-height rectangles slid over each other.** Found while building the resize stops; the slide-over case now counts as an overlap everywhere the check is consulted, including the draw tool. - **Fix (HP-1543-01): editor zoom stayed on screen after a floor switch made inside the editor.** Exiting an editor on a different floor than the one you started on skipped the viewport restore and left the editor working zoom in view mode. Such an exit now falls back to the current floor's saved view zoom. - **Fix (HP-1543-02): a motion sensor re-tripped mid-flash stayed invisible.** A rapid off→on before the current flash ended kept the old CSS animation timeline, so the second detection played nothing. Every witnessed trip now gets a fresh animation identity and the flash restarts; under reduced motion the static ring stays, as before. ## v1.54.3 — 2026-08-01 Patch release: three quality-of-life rounds — icon satellites now follow the per-device size multiplier, editor zoom stopped leaking into view mode, and motion sensors got a visual language of their own. - **The per-device size multiplier now scales the icon's satellites too.** Reported by an owner with a screenshot: shrinking a device left the value badge, the temperature/humidity plates, the LQI label, the «new» dot and the alarm ring at full size, towering over the tiny icon. Every satellite now follows the multiplier along with the icon. - **Editor zoom is a working tool, not a saved setting.** Zooming to 500% to place a marker precisely and then closing the editor used to drop you into a 500% view mode. Leaving any editor now restores the viewport you had in view mode before you started editing. - **Fix: editor zoom could still resurrect when switching floors.** The editor viewport raced the per-floor viewport store and won: switch floors while editing and the 500% was written down as that floor's saved view, greeting you on the next visit (steps to reproduce came from the owner). Editor zoom is never written to the store anymore. - **Tripped motion and presence sensors signal with a yellow ring.** The rule stays «fill = on»: motion is a one-shot flash — three pulses at the moment of detection, with no pulsing during the cool-down tail — while occupancy/presence hold a static ring for as long as they report someone present. - Stand only: a `services.yaml` for the demo stand's `demo_guard` to keep hassfest green; nothing of it ships in the integration. ## v1.54.2 — 2026-07-31 Patch release: the one remaining finding of the v1.54.1 re-audit (HP-1541-01), pinned by regression tests on both sides of the contract. - **Fix: a vacuum whose own `selected_map` is `0` split calibration and trail between two map ids.** The v1.54.1 map-id contract («the first value that exists wins, and zero is a value») was applied to the source entity but not to the card's fallback on the vacuum's `selected_map`: the frontend still judged it by truthiness and turned `0` into `default`, while the server recorder stored the run under `0`. Calibration was saved under a key the recorder never used, and the recorded trail never rendered after a reload. The fallback now follows the same not-nullish rule on both sides (`vacMapIdWithFallback` in the card, `resolve_map_id` on the server), with cross-runtime regressions for `selected_map` = `0`, `"0"` and `""`. ## v1.54.1 — 2026-07-31 Patch release: everything the adversarial audit of v1.54.0 found (HP-1540-01..06), each fix pinned by a regression test that fails on the old code. - **Fix: the first calibration of a freshly discovered vacuum silently did nothing.** Until the device dialog was saved once, the robot had no config marker — yet the «Живая позиция» section was fully interactive, and every handler quietly bailed out while auto-calibration still announced success. Any vacuum edit now materialises the marker itself, and the success toast only appears after the matrix has verifiably landed in the config. - **Fix: a robot whose first map is `map_index: 0` lost its server-side trail.** The backend picked the map id by truthiness and dropped the zero, so the recorded run was stored under a key the card never looked up. Both sides now share one explicit rule — the first value that exists wins, and zero is a value. - **Fix: one robot on two floors recorded history for the last floor only.** The recorder kept a single marker per source entity, so the second placement silently evicted the first. Every marker fed by a source now gets its own copy of the run. - **Fix: auto-calibration ignored plans drawn with rectangle rooms.** The room matcher only accepted polygon outlines and then blamed the room names. Legacy rectangles count like everywhere else in the card. - **Fix: overlapping recorder refreshes leaked state subscriptions.** Two config saves racing each other could both subscribe and strand one callback until restart; refreshes are serialised now and teardown wins over any refresh still in flight. - The «no rooms» / «no match» toasts and docs/VACUUM.md no longer send you to the three-point calibration that no longer exists — they point at «Подогнать вручную», which does. ## v1.54.0 — 2026-07-31 ### Live robot vacuums The plan now shows the robot while it works. The device marker stays where you put it — that is the dock — and a round puck drives the plan in real time, pouring its path out from under itself. Everything is display only: the card never commands the robot. - **Calibration without arithmetic.** «Настроить автоматически» matches the robot's room list against your rooms by name and solves the transform in one click. When names do not match, the fit panel lays the robot's rooms over the plan as a dashed ghost: drag it into place, stretch it by the corner handles, quarter-turn and mirror with two buttons. Mirror is on by default — every robot map we have measured flips Y versus the screen. One calibration per robot map, so two floors stay independent. - **The path is recorded server-side.** The integration watches the robot itself, so the trail records with no card open, survives page reloads, and every screen sees the same line. The current run and one previous run are kept — cleaned versus not-yet-cleaned at a glance. - **«Показывать путь робота»**: never / while cleaning (default) / always. Only the last mode also draws the previous run, faded. - The trail never runs ahead of the icon: drawn segments lag one point and the growing tip is glued to the puck's animated centre every frame. It is drawn as a dark halo under a light core, so it stays readable over any room fill. The puck teleports instead of gliding when the view changes — zoom, floor switch or a return to the browser tab. - Adapters: Xiaomi Cloud Map Extractor, dreame-vacuum (Tasshack), Valetudo. Verified against a live Dreame X50 Master. ### Also - **«Свет по источникам» is the default fill for new spaces** and leads the options list. Existing plans are untouched: a space whose owner never chose a fill still renders as before. - **Fixed: the "what to run" search showed no results.** The list is a scrollable box and, as a flex item, collapsed into a 1px sliver — the matches were there, rendered into nothing. ## v1.53.1 — 2026-07-30 - **Fix: the "what to run" search showed no results.** The results were there — the list is a scrollable box, and as a flex item inside the dialog body it collapsed into a 1px sliver, so 26 matching automations rendered into nothing visible. Reported within minutes of v1.53.0 by the owner. The list keeps its height now; the smoke measures that height instead of merely counting DOM rows, which is why it passed the broken build. ## v1.53.0 — 2026-07-30 **A tap can run your automation** (owner's spec) - **New tap action: "Run automation/script/scene".** We cannot know every exotic device, but you know what a tap on it should do: pick any automation, script or scene from a searchable list, and the tap runs it — `automation.trigger`, `script.turn_on` or `scene.turn_on` per kind, with a "Started" toast. A script is the idiomatic HA "action", so all three runnable kinds are offered — no trigger-less dummy automations needed. Saving refuses a run action without a target; a target deleted later warns in the dialog and toasts safely on tap. - **"Ask for confirmation" checkbox** — the accidental-tap guard, available for any state-changing action: toggle and run alike, wall-switch markers with bound targets included. The dialog is the card's own (Esc, backdrop or Cancel = nothing happens), so it works on a wall tablet. - **Curtains toggle natively:** covers and valves joined the card-wide toggle domains — with one deliberate exception: garage doors, doors and gates (cover device classes garage/door/gate) stay OUT of the default toggle, an accidental tap must not open the driveway. An explicit per-device toggle remains the owner's conscious choice, and locks/alarm panels stay untouchable from the plan, as always. ## v1.52.2 — 2026-07-29 **From the v1.52.1 review** (no runtime changes — test and wording quality) - The plan-editor regression check now targets THE lamp (HP-1521-01): the old assertion accepted any yellow badge, and the lit socket in the same fixture would have satisfied it with the lamp fix removed. Verified by mutation: reverting the v1.52.1 gate makes the smoke fail. - The one remaining "yellow in every fill mode" wording — the checklist entry and the _stateClass comment — now states the actual contract: the lit-source state is computed by the glow-pool condition, and the yellow badge shows only where the glow spot is not drawn (HP-1521-02). ## v1.52.1 — 2026-07-29 **From the v1.52.0 review** - **A lit lamp always has exactly one indicator (HP-1520-01).** The glow layer is hidden in the plan editor, but the yellow suppression still applied there — a lit lamp showed neither the spot nor the badge. The suppression gate now equals the layer's actual visibility: wherever the spot is not drawn, the yellow badge returns. - **The static card honours a marker's size and rotation (HP-1513-01).** The same stored marker rendered at base size, unrotated, on the read-only card. It now mirrors the full card's --dev-scale and angle — geometry only, still a schematic. - Documentation caught up with the v1.52.0 colour contract (HP-1520-02): the RGB-tint expectations in TESTING/UX-MODES and a stale inline comment. ## v1.52.0 — 2026-07-29 **One look for light sources, whatever flipped them** (owner's rule) - **A lamp's colour lives only in its glow.** The RGB tint of the icon, border and shadow is gone: depending on whether the state carried colour data, the same lamp used to land in the "coloured icon on a dark badge" or the "plain yellow badge" branch — turning one lamp off by tap and the rest by the wall switch produced visibly different results. The branch is gone. - **In glow fill the indicator IS the glow spot.** A light source's badge stays standard, lit or not — the pool of light around it says everything. A lit socket, fan or kettle keeps its yellow even in glow fill: they cast no light, the rule is for sources only. - **In every other fill a lit source is plain yellow**, like a heating radiator valve — RGB and white lamps alike. - Icon morphing (the shining-bulb outline) stays in every mode, and the ripple colour still falls back to the lamp's light colour. ## v1.51.3 — 2026-07-29 - **The icon size multiplier scales the glyph, not just the badge.** Changing a device's size grew the badge, the ripple and the value badges but left the icon itself at its default size — a big empty box around a small glyph (user report). The glyph now derives from the same per-device size as everything else and keeps its proportion at any multiplier and zoom. ## v1.51.2 — 2026-07-29 **From the v1.51.1 review** - **The auto grid is the same on both cards (HP-1511-01).** The full card reserves grid cells for hidden devices (their ghosts keep a place in the device editor); the static card compacted the grid over visible ones only, so a freshly discovered marker with no saved position landed in different spots on the two cards. The static card now feeds the full roster to the same grid and still draws only the visible. - **A ripple-display ghost keeps its base icon (HP-1511-02).** Hidden markers with the "ripple" presentation rendered as an icon-less inactive pulse — unrecognisable in the editor. A ghost now drops the display dressing entirely: base icon and name, whatever the display mode. ## v1.51.1 — 2026-07-29 **From the v1.51.0 review** - **The static card counts hidden devices in room LQI again (HP-1510-01).** Its visibility filter had quietly become the aggregation filter: the same room showed different Zigbee health on the two cards. Aggregation and rendering use separate lists now — hidden devices count toward signal on both cards, are drawn on neither, and still cast no light. - **A ghost shows no live numbers (HP-1510-02).** A hidden device in "Show hidden" suppressed the state colors but still painted its value text, temperature, humidity, LQI badge and state-morphed icon. All of that is gone: the ghost keeps only the base icon and name — enough to recognise it and open the dialog. ## v1.51.0 — 2026-07-29 **Hiding is an explicit flag now** (docs/FILTERING.md) - **Every device dialog — virtual ones included — has a "Hide device from plan" checkbox.** The old on-the-fly filter survives only as the SEEDER of those flags: on first load by an editing client the config is materialised once — non-physical devices (bridges, scenes, service integrations, lamps folded into a light group) get the flag, and from then on the flag belongs to you. Unticking it is final: the seeder never revisits a device you have decided about. New non-physical devices hide silently; physical ones keep the red-dot flow. - **"Show all" became "Show hidden"** — a local tool of the device editor (nothing flips on the wall tablets), showing hidden devices as translucent BLUE dashed ghosts: clearly apart from a grey unavailable icon, and with no live-state paint at all — a ghost is configuration, not status. Click one to untick the box. "Remove from plan" is gone for bound devices (the checkbox is the way); a virtual device's Delete still deletes. - Hidden devices still count toward the room's Zigbee signal, but cast no glow and no light fill — an invisible device casts no visible light. Room climate is unchanged. Old configs behave exactly as before until an editing client materialises them. **Yellow means working right now** - One principle for the glowing icon: a light is shining, a socket is powering, a fan is spinning, media is playing, a vacuum is cleaning — or a radiator valve is ACTUALLY heating (hvac_action), not merely enabled for the winter. Previously a TRV could glow yellow because its anti-scaling service switch was on while the actually-heating one stayed dark: the primary-entity search let a vendor's config switch outrank the visible climate entity. Fixed — a service entity never beats the device's visible main function (this also fixes tap-toggle and icon morphing on such devices). - The glow pool and the icon color now ask the same question: a lit light yellows its icon in every fill mode, by exactly the condition that lights its glow spot. The README (en+ru) documents the color language. **The editors, on a phone** - Pinch zoom and pan gestures now work in every editor: drawing is click-based, so the two coexist — a moving finger pans, two fingers pinch, releasing after a gesture never draws a point, a clean tap still does. **The room settings button** - Detached from the (movable) room name: it sits at the VISUAL centre of the room — the centre of the largest inscribed circle with a pull toward the area centroid, so an elongated room centres it on both axes and an L-shaped one keeps it in the middle of its widest part, never down a thin limb. - Half its former size, sized from the device icon (70% of the icon box) and zooming WITH the plan instead of keeping a constant screen size. - The small metric rows under the room name (temperature, humidity, signal, lights) now show in the plan editor too, and the name renders in exactly the same spot in view mode and in the editor. ## v1.50.4 — 2026-07-29 **From the v1.50.3 review** - **Both cards build their model with the same code now (HP-1503-01).** The full card carried a hand-copied twin of the shared model builder, and the twin missed the legacy-store fallbacks v1.50.3 added — the same broken store rendered fine in the static card and as a blank `viewBox="0 0 0 0"` in the main one. The duplicate is gone: the full card calls the shared builder and only swaps in the raw plan url its signing flow needs. A new smoke runs the audit's exact legacy vector through both models and both DOM trees and asserts parity. ## v1.50.3 — 2026-07-29 **From the v1.50.2 review** - **A size is not a coordinate (HP-1502-01).** The ±4 bound from v1.50.2 treated all four view_box elements and room w/h alike, so `[0, 0, 0, 0]` and negative sizes still passed — and a zero axis serialises into `viewBox="0 0 0 0"`, a blank plan on every client, with the static card computing `aspect-ratio: 0 / 0` on top. Sizes now get their own validator: strictly positive, floored at one thousandth of the canvas; coordinates may still be negative, because a crop origin legitimately sits past the edge. And since a store may already hold a broken viewport from before, both cards fall back to the whole canvas instead of a blank screen, and a legacy rectangle with a negative size is read as the same rectangle drawn from the other corner. **Also in this release** - The room settings button moved to the bottom of the room card, and the room name renders in exactly the same spot in view mode and in the plan editor — the button and the metrics no longer take part in the label's centring. ## v1.50.2 — 2026-07-29 **From the v1.50.1 review** - **Geometry magnitudes are bounded on both layers (HP-1501-01).** v1.50.1 bounded layout positions, but room rectangles, polygon vertices, view_box and opening coordinates still took any finite float — one schema-valid 1e100 vertex framed the space so wide the plan was a dot, for every client, and the server stored it as a perfectly good configuration. The config schema now bounds geometry to ±4 (angles to ±360°), and the content frame applies the same canvas envelope to room vertices it already applied to device positions — so a store that already holds an absurd coordinate from before this door existed still renders: the point draws wherever it is, it just no longer commands the frame. A vertex a bit past the canvas edge keeps working. - **A no-op repair no longer eats the undo backup (HP-1501-02).** A typo'd space id "succeeded" with moved: 0 — and its empty result replaced the one-deep backup, destroying the only way back exactly when it was needed most: right after repairing the wrong space. Matching nothing is an error now (`nothing_to_repair`); nothing is written, the revision does not move, and the previous repair stays undoable. ## v1.50.1 — 2026-07-29 **From the v1.50.0 review** - **A card below other dashboard content gets its stage back (HP-1500-02).** The v1.50.0 height measurement used the absolute document coordinate, so a tall card before this one was billed as "header" and the stage collapsed to zero. The card now measures only its own chrome plus a bounded allowance for what the dashboard keeps above it, and re-measures on window resize; the listener is removed on teardown. - **The content frame can no longer be degenerate or absurd (HP-1500-03).** A lone marker in an empty space produced a zero-area viewBox — a blank scene; a single stored coordinate like 1e100 (any finite float passed validation) stretched the frame until the plan was a dot, for every viewer of the space. A near-zero axis now opens up to a floor of canvas around the marker, points far outside the canvas envelope no longer command the frame (they still render where they are), and the server refuses layout coordinates outside ±4 — generous slack for an icon dragged past an edge, not an envelope for absurdity. A real thin room keeps its tight frame, and the gate sensor slightly past the edge still counts. - **A repair path for installs stranded by the v1.48 migration window (HP-1500-01).** If the old migration crashed between its two writes, the markers of a space are left in the old coordinates with nothing in the data able to prove it — and re-transforming a correct layout would corrupt it, so nothing automatic is safe. `houseplan/geometry/repair {space_id, aspect}` is the explicit answer: `dry_run` previews the exact moves, the previous positions ride the same store write as a one-deep backup, `undo` restores them, and routine drags no longer erase that backup. The v1.50.0 `geom_pending` protocol already protects every future migration; this covers the installs it was too late for. ## v1.50.0 — 2026-07-28 **Owner's batch** - **The default zoom counts devices as content.** They are allowed to stand outside every room — a gate sensor by the fence, a camera on a pole — and the opening view now includes them, even on a space with no rooms at all. - **Entering an editor no longer shifts the plan.** The stage height assumed a fixed 118px of header, and the editor header is taller: the scene slid down by the difference and its bottom went below the fold. The card measures where the stage actually starts and gives it the rest of the viewport. - **The zoom goes out as well as in.** Down to 0.4×, and zoomed out the plan floats centred instead of being pinned to a corner. **From the v1.49.0 review** - **The square-canvas migration survives a crash between its two writes (HP-1490-01).** Config and layout live in separate stores, written one after the other, and the first write deleted the very fields the second needed — a failure between them stranded markers in the old coordinates for good. The migration intent is durable now, saved before anything moves and cleared by the layout write itself; whichever half is missing after a crash, the next start finishes exactly that half, once. - **Parallel uploads cannot slip past the store quota together (HP-1490-02).** N uploads all measured the store before any of them wrote, and all passed a limit only one of them fit under. The measure-and-write pair is one atomic step under its own lock — separate from the config lock, so a slow directory scan does not stall saves. - **The editors see the whole canvas again (HP-1490-03).** The content frame also bounded pan, zoom and pointer maths, so after the first room there was nowhere left to draw the second one. Edit modes now measure from the full square; the view keeps its content fit, and switching modes refits instead of carrying a view clamped against the wrong base. - **Save waits for the proportions of a picked plan (HP-1490-04).** Saving before the image had answered used to ship the PREVIOUS file's ratio, and the new plan kept the old shape for good. Picking a plan clears the old ratio at once, and Save awaits the bounded read; if it fails, "unknown" is stored — a square fallback is honest, an inherited ratio is not. - Release hygiene from §5: package-lock.json caught up with the package version, and a duplicated comment in space-geometry.ts is gone. ## v1.49.0 — 2026-07-28 **The canvas is square** (see v1.48.0, released together with this one). - **Zoom opens on what is drawn, not on the whole canvas.** A space without a background image now fits its rooms with a 5% margin, so a small plan on a big canvas fills the screen instead of sitting in the middle of it as a speck. With a background image nothing changes: the image is the plan, and cropping to the rooms would hide the parts nobody has outlined yet. - **Switching spaces by swipe, or on the kiosk carousel, slides.** The plan leaves the way the finger went and the next one arrives from the other side. Respects "reduce motion". - The room settings button says "Room settings" rather than just "Room", and lightens slightly under the cursor. - "Curation" is called filtering everywhere — the interface, the documentation and the code. **From the v1.47.0 review** - **A plan you have just picked can no longer be deleted from the same dialog (HP-1470-02).** It was not saved yet, so the server correctly considered it free — and the save then stored a url with no file behind it. The button is disabled now, and, because two clients can do the same in either order, the server checks every internal plan url a configuration adds against the disk and refuses a new reference that is already broken. A url the stored configuration already carries is let through — a file can vanish from outside Home Assistant, and refusing then would block the very edit that detaches it. Urls that are not ours are left alone. - **Uploads are bounded (HP-1470-01).** Nothing is deleted for being old — that cost real plans twice — so the limit sits where a decision is being made anyway: an upload is refused if the store would pass 256 MB or 200 plans (1 GB / 1000 for attachments), or if the disk would drop below 512 MB free. The plan list is capped at the 60 newest and its thumbnails load lazily. - **Picking a saved plan reads its real proportions (HP-1470-03).** The card waited for nothing and, when the signature for the protected url had not arrived yet, saved a fallback ratio — a square plan came out stretched. It now waits for the signature, ties the result to the dialog that asked, and the preview in the dialog is signed like everything else. ## v1.48.0 — 2026-07-28 (the canvas is always square) - **A space no longer has proportions of its own.** The drawing area is a square; a plan image keeps its own shape and is centred inside it, so a wide plan gets margins above and below and a tall one gets them at the sides. There is nothing left to choose — the canvas orientation setting for hand-drawn spaces is gone with it. - **Existing plans are migrated once, on upgrade.** Nothing about a drawing changes: the box is padded out to a square and every coordinate is re-expressed against it — rooms, doors and windows, decor, marker positions and the saved viewport. Angles, room proportions and relative positions are preserved exactly. For a tall plan the scale in centimetres per grid cell is adjusted along with it, because the grid is tied to the width; without that a wall would silently measure less than it does. ## v1.47.0 — 2026-07-28 (pick a plan you already uploaded) - **The space dialog can now show the plans stored on the server.** Detaching a plan keeps the image on disk — that has been the rule since v1.46.4, but until now the only way back was to find the original file on your computer and upload it again. "Already uploaded" lists what is there, with a thumbnail, the file size and which space uses it. One click attaches it; the aspect ratio is read from the image, exactly as on upload. - **And it is where you delete one.** A plan file is never removed automatically — not for being detached, not for being old — which is only a sensible policy if you can see what is being kept and get rid of it deliberately. The trash button does that, and refuses while a space still uses the plan: the answer to "may this go" comes from the stored configuration, not from the browser. - Documentation caught up with the code: several comments still described the age-based collection that v1.46.6 removed. ## v1.46.6 — 2026-07-28 (the detach promise, actually kept this time) - **Switching a space to "draw" no longer deletes its image.** v1.46.4 and v1.46.5 said it did not, and the scheduled cleanup indeed left detached plans alone — but the save itself deleted the file the moment the reference was cleared, before any of those guards were reached. The cause: a file that left the configuration was called "superseded", and from that difference alone replacing a plan, detaching one and deleting its space are indistinguishable. Only the first is a deletion anybody asked for. The transition is now classified by the space that owned the file, and the same distinction applies to attachments: dropping one from a device that still exists removes it, deleting the device keeps its manuals. - **A plan whose space was deleted is kept**, rather than the thirty days v1.46.5 promised — thirty days measured from the file's age is meaningless anyway, since it was usually uploaded months earlier. - **Nothing is deleted for being old any more**, except a per-dialog staging folder. The rule that aged out "rejected uploads" turned out to race a retry: the cleanup removed the file from a failed save while the next attempt was committing a reference to it. A rule that can delete a file somebody is about to point at is not worth the disk it reclaims. Files therefore go when an action says so, and otherwise stay. ## v1.46.5 — 2026-07-28 (audit of every automatic deletion) - **A detached plan is never deleted, at any age.** v1.46.4 gave it a month; this makes it permanent and writes the reason down where the next change will see it. The rule, now in docs/SCOPE.md: the component may delete a file only when a user action says so — replacing a plan, removing an attachment, deleting a device. "Nothing points at this any more" is not such an action. The errors are not symmetrical: wasted disk is visible, cheap and reversible; a deleted file is none of those. - **`houseplan/files/cleanup` no longer takes a folder on the client's word.** After a device is rebound its files are copied to the new id and the old folder is dropped — with `rmtree`, on whatever id the card sent. Two ways that ends badly: a partial copy leaves some urls still pointing into that folder (the migration deliberately does not rewrite those, so they were live links to files being deleted), and a wrong or stale id from any client would destroy a live device's manuals. The server now checks the stored configuration itself, under the config lock, and removes only files nothing references. - **A plan of a space that was deleted waits thirty days instead of an hour.** Deleting a space is deliberate, but an hour is a short window in which to notice it was a misclick. ## v1.46.4 — 2026-07-28 (data loss: detached plans were collected as garbage) - **A plan you detach is no longer deleted an hour later.** Switching a space to "draw" clears the reference and, as the editor has always said, leaves the image on disk so you can put it back. The collection added in v1.46.0 did not make that distinction: it treated "nothing points at this right now" as abandoned and applied a one-hour rule. On the author's own instance the scheduled pass then removed two floor plans that had been detached weeks earlier, with no way to get them back. If you have detached a plan since v1.46.0 and your instance restarted or ran for a day, check `config/houseplan/plans/` before updating anything else — and please report it in the Telegram chat if a file is missing. The rule now: **a commit still removes exactly what it replaced**, because that it knows for certain. Beyond that the question is whether "unreferenced" means "abandoned", and the answer depends on the case. A space with no plan at all has had one detached and may want it back — its files are never collected. A space that does have a plan can only be holding rejected uploads of its own, so those still go after an hour. Attachments outside a per-dialog staging folder wait a month; a staging folder, which by construction only ever holds an upload from a dialog that was never saved, keeps the one-hour rule. ## v1.46.3 — 2026-07-28 (re-check of v1.46.2: HP-1462-01) - **The cleanup at startup now actually cleans up.** It looked its own runtime data up by domain, and during startup Home Assistant does not yet consider the integration loaded — so the lookup came back empty and the pass quietly degraded to removing half-finished transfers, leaving the real work to a timer 24 hours away. Restart more often than that and it never ran at all. It uses the object it was given at startup now. - **The test that was supposed to prove this was passing for the wrong reason.** It created the stray files *before* saving the configuration — and saving collects too, so everything was already gone by the time the restart happened. Rewritten to seed after the save, plus a second test that fires the scheduled timer on its own, and a third that runs a restart and a save at the same time and asserts the accepted configuration never points at a file the cleanup removed. ## v1.46.2 — 2026-07-28 (re-check of v1.46.1: HP-1461-01, -02) - **A file nobody ended up using is now cleaned up even if nothing is ever saved again (HP-1461-01).** Collection is tied to a configuration write, which is right for what a write supersedes but leaves a gap: cancel a dialog after the file has already uploaded, lose the connection just after, or call the upload API directly, and nothing references the file and no future write notices it. The daily sweep added in v1.46.1 only removed half-finished transfers, so the promise that a cancelled attachment disappears after an hour did not hold on an instance nobody edits. The sweep now compares against the stored configuration — under the same lock a write uses — and collects aged unreferenced attachments and plans as well. - **A drag is no longer undone by someone else's move (HP-1461-02).** When the full card learned to follow position changes in v1.46.1, it protected the positions you had moved but not yet sent — except it read that list *after* flushing the pending write, and flushing empties it first. In a real drag, where a write is already scheduled, the list was therefore empty and the server's older position was painted over your move. The card now takes the snapshot before flushing and also holds on to positions that are sent but not yet acknowledged: until the server confirms a position, the card that moved it is the authority on it. - Two tests grew up to their docstrings: the upload test now actually cancels the request task instead of only exercising error paths, and the position-sync smoke schedules a real debounced write and delays it, which is the ordering that lost the drag. ## v1.46.1 — 2026-07-28 (re-check of v1.46.0: HP-1460-01 … -03) - **Two uploads of the same file name can no longer collide (HP-1460-01).** v1.46.0 stopped overwriting attachments, but choosing a free name and taking it were two steps: two uploads racing between them agreed on the same name, both reported success, and one set of bytes replaced the other. The name is now claimed atomically as it is chosen — twenty simultaneous uploads of `manual.pdf` produce twenty files. The same helper is used when rebinding moves files, which had the same gap. Also fixed there: a name at the length limit lost its extension, and the collision suffix pushed it past the limit, so the attachment was stored under a name the server would not serve back — a permanent 404 on a file the UI reported as attached. - **An interrupted upload no longer leaves a temporary file forever (HP-1460-02).** Cleanup ran in an `except Exception`, which a cancelled request walks straight past, and the collector only ever looks inside marker folders — so an aborted transfer left a `.upload-*` in place with nothing able to remove it. Every exit path now cleans up, a request carrying two files is refused outright, and abandoned temporaries are swept at startup and daily. Uploads also write in 1 MB batches instead of one disk task per 64 KB. - **Two full cards side by side keep the same positions (HP-1460-03).** v1.46.0 taught the static card to follow position changes and left the full one behind, so dragging an icon in one window did not move it in another until a reload. It follows now, without disturbing a drag of its own: a revision arriving mid-drag is merged rather than applied over the top, and a card does not re-read what it just wrote itself. ## v1.46.0 — 2026-07-28 (full external audit of v1.45.4: HP-1454-01 … -10) **Security** - **An uploaded SVG plan is no longer a live document of your Home Assistant origin (HP-1454-01, high — release blocker).** Inside the card a plan is referenced by ``, where scripts never run; but the same url opened directly became a top-level document of HA's own origin, and a `