# Changelog ## 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 `