100 Commits
Author SHA1 Message Date
Matysh f36d2cddad v1.53.1
Validate / smoke (push) Failing after 8m31s
Validate / hacs (push) Failing after 9s
Validate / hassfest (push) Failing after 11s
Validate / frontend (push) Successful in 1m50s
Validate / backend (push) Failing after 6m15s
2026-07-30 01:50:20 +03:00
Matysh 1770ca2960 v1.53.1: the run-target search rendered into a 1px sliver
Reported by the owner minutes after v1.53.0: typing a name showed no
results. The results existed — .candlist is a scrollable box, and a
scrollable flex item inside the dialog body collapses happily: 26 matching
rows rendered inside a 1px strip. The binding dropdown never showed this
because it sits inside .droppanel, a block context.

flex: 0 0 auto + a min-height keeps it open. The smoke now MEASURES the
list and the first row instead of counting DOM nodes — counting is exactly
why it passed a build where nothing was visible.
2026-07-30 01:47:19 +03:00
Matysh 764e023996 v1.53.0 2026-07-30 01:37:00 +03:00
Matysh ce6a11f53f v1.53.0: a tap can run your automation
The owner's spec shipped in dev yesterday, released as one:
- tap action 'Run automation/script/scene' with a searchable picker,
  per-domain services, save/runtime guards for the target
- 'Ask for confirmation' checkbox guarding toggle and run alike
- covers/valves in the card-wide toggle, garage/door/gate excluded

Inventory: 148 / 52 / 43 / 72.
2026-07-30 01:34:05 +03:00
Matysh 3e976562ff tap action: run an automation, a script or a scene — with a confirm guard
Owner's spec (2026-07-29), agreed points: one 'Run' action covering the
three runnable domains of HA (a script is the idiomatic 'action' — with
automations alone people would build trigger-less dummies); the confirm
checkbox guards BOTH toggle and run; covers and valves join the card-wide
toggle so curtains work natively.

- marker.tap_action gains 'run'; marker.tap_target (schema-bounded to
  automation./script./scene. ids); marker.tap_confirm.
- the dialog: a searchable picker over the three domains (friendly name +
  kind), save refuses a run action without a target, a vanished target gets
  a warning hint; the checkbox shows for any actionable tap (explicit or
  effective-default toggle).
- the tap: automation.trigger / script.turn_on / scene.turn_on, started/
  error toasts; with confirm on — our own dialog (not window.confirm, it
  must work on a wall tablet), Esc/backdrop/Cancel = no call. The guard
  covers the controls-toggle path too.
- 'run' is explicit-only by construction: it needs a per-marker target, so
  it can never arrive as a card-wide default.
- covers: the old test pinned 'garage stays shut' — that intent survives as
  COVER_GUARDED_CLASSES (garage/door/gate stay out of the CARD-WIDE toggle;
  an explicit per-device toggle remains the owner's conscious choice).
  Locks/alarms stay forbidden everywhere, run included is not affected —
  we do not inspect automation contents, same trust as HA's own Run button.

Tests: unit resolveTapAction/runServiceFor + cover guard, backend schema
parity picks 'run' automatically + tap_target bounds, smoke_tap_run with 11
assertions (picker, search, save guard, confirm cancel/ok, per-domain
services, missing target). smoke_tap_ctx: 4 options now.
Inventory: 148 / 52 / 43 / 72.
2026-07-30 01:26:30 +03:00
Matysh f56bceef27 STATUS: fresh HACS queue numbers (2026-07-29) — 835 ahead, merge rate stalled 2026-07-30 00:49:51 +03:00
Matysh 96a01e1380 v1.52.2 2026-07-29 22:19:22 +03:00
Matysh 08a9cc7d27 v1.52.2: the v1.52.1 review (HP-1521-01, HP-1521-02)
- HP-1521-01: the plan-mode assertion looked for ANY .dev.on and the lit
  kettle satisfied it — a false positive hiding the very regression it
  guards. It targets d_lamp now (kettle asserted separately), and the
  mutation check proves it: reverting the v1.52.1 gate fails the smoke.
- HP-1521-02: the checklist entry and the _stateClass comment still said
  'yellow in every fill mode'. Both now state the two-part contract: the
  state predicate is the glow-pool condition; the renderer keeps the badge
  only where the spot is not drawn.
2026-07-29 22:16:26 +03:00
Matysh 1be79a1427 v1.52.1 2026-07-29 21:58:05 +03:00
Matysh d7c20ff6a5 v1.52.1: the v1.52.0 review (HP-1520-01/-02, HP-1513-01)
- HP-1520-01: the glow layer is hidden in the plan editor, but the yellow
  suppression still fired there — a lit lamp had NEITHER indicator. The
  gate now equals the layer's visibility (disp.fill === 'glow' &&
  !this._markup), so the badge returns exactly where the spot is absent.
- HP-1513-01: the static card ignored marker.size and marker.angle — the
  same stored marker looked different on the two cards. It mirrors
  --dev-scale and the icon rotation now; geometry only, no live dressing.
- HP-1520-02: TESTING/UX-MODES still demanded the removed RGB icon tint,
  and the lightC comment described the old use. All three brought to the
  v1.52.0 contract.

smoke_light_badges grew the editor-mode vectors; new
smoke_size_angle_parity asserts the x3 ratio inside each card (absolute px
are incomparable across containers) and rotation on both. Inventory:
147 / 51 / 43 / 71.
2026-07-29 21:55:13 +03:00
Matysh fa59767c69 v1.52.0 2026-07-29 21:34:23 +03:00
Matysh 60d6167ecd v1.52.0: one look for light sources, whatever flipped them
Owner's rule, agreed 2026-07-29 after a field report (a lamp turned off by
tap looked different from one turned off by the wall switch):

- a lamp's colour lives ONLY in its glow. The v1.27 RGB tint of the icon,
  border and shadow is deleted — that tint was the fork: with colour data
  the lamp rendered dark-with-coloured-icon, without it plain yellow, and
  the same lamp crossed the fork depending on how it was switched.
- in glow fill the indicator IS the spot: a source's badge stays standard,
  lit or not (litLightEntity — the exact condition that casts the spot —
  gates the suppression, so a lit socket keeps its yellow even in glow).
- in every other fill a lit source is plain yellow, like a heating TRV.
- icon morphing stays everywhere; the ripple colour still falls back to the
  light colour (both explicitly confirmed by the owner).

smoke_light_badges covers the whole table (8 assertions); smoke_rgb_alarm
re-asserted: no rgb class, lit lamp yellow, ripple fallback keeps the
colour. README colour language updated. Inventory: 147 / 51 / 43 / 70.
2026-07-29 21:31:30 +03:00
Matysh 110fabd038 v1.51.3 2026-07-29 21:08:58 +03:00
Matysh 3456706ef6 v1.51.3: the size multiplier reaches the glyph (user report) 2026-07-29 21:06:10 +03:00
Matysh 1310c84a32 device icon: the size multiplier scales the glyph, not just the badge
User report via the owner: change a marker's size and the icon stays at its
default size — a big empty box around a small glyph. The badge, ripple and
value badges all derive from --dev-size (base size x per-device multiplier),
but --mdc-icon-size was pinned to the BASE --icon-size, so the multiplier
never reached the glyph. One calc argument: --dev-size.

New smoke_icon_scale: at size 3 the glyph grows with the badge and keeps
the 0.62 proportion. Inventory: 147 / 51 / 43 / 69.
2026-07-29 21:03:12 +03:00
Matysh 94563d2a0c v1.51.2
Validate / hacs (push) Failing after 25s
Validate / hassfest (push) Failing after 18s
Validate / frontend (push) Successful in 1m38s
Validate / smoke (push) Failing after 8m4s
Validate / backend (push) Failing after 6m43s
2026-07-29 18:03:15 +03:00
Matysh 5615afa33b v1.51.2: the v1.51.1 review (HP-1511-01, HP-1511-02)
- HP-1511-01: defaultPositions ran over different rosters — the full card
  reserves grid cells for hidden devices, the static card compacted them
  away, so an undragged marker sat in different spots on the two cards. The
  static card feeds spaceDevs (hidden included) to the shared grid and
  renders devs (visible) — exactly the split HP-1510-01 introduced for LQI.
- HP-1511-02: a hidden ripple-display marker rendered as an icon-less
  inactive pulse. A ghost drops the display dressing entirely: ripple
  presentation off, noicon off, base icon on, whatever marker.display says.

smoke_hidden_flag: the weak 'has icon OR noicon' assertion is gone — every
ghost must carry a base icon; new autoGridParity vector (vb-coordinate
comparison, the cards render in different view systems) and a ripple-ghost
vector. The demo stub got a connection.subscribeEvents so the static card's
module-level config cache can be invalidated between in-test cards.
2026-07-29 18:00:22 +03:00
Matysh 4083e14247 docs: everything caught up with v1.51.1
A sweep of every document against the shipped behaviour:

- README en+ru: the space dialog no longer claims a background is mandatory
  (draw-by-hand and the saved-plans picker exist; the canvas is square);
  'Show all devices' sections rewritten for the hide-flag world — the
  checkbox, 'Show hidden' ghosts, LQI-yes/light-no; troubleshooting updated.
- ARCHITECTURE: the config schema block still described v1.3 —
  aspect/device_overrides/virtual_devices/1000x1000-per-aspect/legacy-bundle
  fallback, all long gone. Rewritten to the current shape (square canvas,
  markers with hidden, filter_seeded, quotas, signed urls). The WS table
  dropped houseplan/file/set (removed in v1.10.0) and gained
  geometry/repair, layout/delete, files/migrate, files/cleanup,
  content/sign, the plans/list cap.
- UX-MODES: the Devices-tab tool list names the checkbox and the local
  'Show hidden' instead of the retired shared show-all.
- ROADMAP: repair-issues, system_health, floors import, data icon rules,
  click actions, theming and JSON i18n were done releases ago — checked off
  with their versions; the HACS pointer is #9004 (bot closed #8995).
- STATUS: SSH port is 22222 and the HA config root is
  /mnt/data/supervisor/homeassistant (/config does not exist there); the
  key lives in houseplan/.secrets; the PAT note reflects the fine-grained
  token; the feature surface gained the v1.42-v1.51 era.
- DEVELOPMENT: deploy instructions with the real port, path and cache
  busting.
- The owner's product description (user folder) refreshed the same way:
  square canvas, hide flags, the yellow principle.
2026-07-29 15:51:07 +03:00
Matysh 518d72fb74 v1.51.1 2026-07-29 15:16:59 +03:00
Matysh fc95a1f09b v1.51.1: the v1.51.0 review (HP-1510-01, HP-1510-02)
- HP-1510-01: the static card's visibility filter had quietly become its
  aggregation filter — the same room showed different Zigbee health on the
  two cards. Two lists now: aggregation (room LQI, temp) sees every device
  of the space including hidden ones, rendering sees visible only. Light
  fill keeps excluding hidden through areaLights itself, so the contract
  stays exactly as agreed: hidden counts toward signal, casts no light.
- HP-1510-02: the ghost suppressed state colors but still painted value
  text, temperature, humidity, the LQI badge and the state-morphed icon.
  All live numbers are gated on d.hidden now — a ghost is the base icon and
  the name, nothing else.

smoke_hidden_flag grew both audit vectors: the 42 kW value-display ghost
renders no numbers, and a room whose only Zigbee devices are hidden paints
the identical lqi fill on the full and the static card.
2026-07-29 15:14:06 +03:00
Matysh 4e736d49a3 v1.51.0 2026-07-29 14:07:45 +03:00
Matysh aa3c379540 v1.51.0: explicit hide flags, the yellow principle, phone editor gestures, room button
The dev batch since v1.50.4, released as one:
- hiding is a per-device checkbox seeded once from the old filter
  (docs/FILTERING.md); blue ghosts under a local 'Show hidden' toggle
- yellow = doing its main job now; TRVs glow by hvac_action, service
  switches can no longer become a device's primary
- pinch/pan gestures in every editor on touch
- the room settings button: visual centre (inscribed circle + centroid
  pull), icon-derived size, zooms with the plan; metrics visible in the
  plan editor

Inventory: 147 frontend / 51 pure / 43 harness / 68 smokes.
2026-07-29 14:04:49 +03:00
Matysh fa15598e67 room settings button: centroid pull breaks the plateau tie
The inscribed-circle criterion is FLAT along the long axis of any elongated
room — every midline point fits the same circle — and a plain argmax took
the first plateau sample: left of centre on the owner's kitchen-living
room, above centre in the sauna. The score now subtracts a soft pull
toward the area centroid (shoelace-weighted): on the plateau the nearest-
to-centroid point wins, while real clearance differences still dominate,
so the point never wanders into a thinner limb of an L.

Verified on a replica of the owner's floor: kitchen slab centre within a
few units, sauna dead-centre both axes. Units: wide and tall rectangles
centre on both axes; the L keeps to its slab near the centroid x.
2026-07-29 14:00:52 +03:00
Matysh 9eec856d50 room settings button: the VISUAL centre for L-shaped rooms
The owner's kitchen-living room is L-shaped, and interiorPoint() only
promises 'somewhere inside' — the button sat near the seam, visibly
off-centre. poleOfInaccessibility() (largest inscribed circle, grid search
plus one refinement pass) puts it in the middle of the widest open space:
the slab of an L, the exact centre of a rectangle or square. Cached per
poly array in a WeakMap — the memoized model keeps the arrays stable, so
the search runs once per geometry, not per render.

Unit: square -> centre; thick-slab L -> mid-slab, always inside.
2026-07-29 13:55:03 +03:00
Matysh 0bb9282edd room settings button: half size, dead-centred on the room
Owner's follow-up: the button was too large — height is now 0.77 of the
icon-size unit (half the previous), width follows through the derived font
and padding. The below-centre offset is gone: the anchor is the room's
geometric centre on BOTH axes, verified against the polygon centroids in vb
coordinates (exact match on all four demo rooms). Still zooms with the plan.

smoke_feedback_v2's gear assertions pinned the 2026-07-27 feedback sizes
(font >= 10px, box >= 18x40) — superseded by the owner's half-size order;
the contract is now 'sized from the device icon, clickable, opens the
dialog'.
2026-07-29 13:47:20 +03:00
Matysh 09f4dc4115 room settings button: room-centred, icon-sized, zooms with the plan
Owner's spec: the button is no longer glued to the room NAME (which the user
can drag anywhere) — it anchors to the geometric centre of the ROOM
(interiorPoint for polygons, so an L-shaped room gets a point actually
inside it), one button-height below centre so it never covers the name,
whose default position is that same centre. Height is 70% of a device icon
box, and since --icon-size already rescales with the view, the button zooms
with the plan instead of keeping a constant screen size (verified: x2.2 zoom
-> x2.20 button). The small metric rows under the room name (temperature,
humidity, signal, lights) now render in the plan editor too — they used to
be view-mode only.

smoke_room_cards updated: plainInPlan now asserts metrics ARE present in the
editor (the old assertion pinned the old behaviour), plus gearDetached.
2026-07-29 13:39:46 +03:00
Matysh 2551b4ea0e hidden devices: blue ghosts, no live-state paint
A hidden device and an unavailable one both rendered as translucent dark —
indistinguishable at a glance, and a lit hidden lamp still glowed yellow
through the ghost (owner's report). A ghost is CONFIGURATION, not status:

- blue dashed ghost (accent-tinted, color-mix with an rgba fallback for old
  WebViews), clearly apart from the grey 'unavailable' icon;
- no state classes, no RGB tint, no alarm pulse, no active ripple on hidden
  devices — the only thing a ghost says is 'I am hidden, click to unhide'.

smoke_hidden_flag grew two assertions: the ghost carries no state classes
and is blue/dashed.
2026-07-29 11:49:37 +03:00
Matysh 694e1e9a3b filtering: hiding is an explicit per-device flag (docs/FILTERING.md)
Agreed with the owner: whether a device is on the plan is a CHECKBOX
('Hide device from plan', every kind incl. virtual), not a runtime
algorithm. The old filter survives only as the SEEDER of those flags.

- marker.hidden is the flag; hidden devices are BUILT (room LQI counts
  them — owner's decision) but rendered only in the device editor with
  'Show hidden' on, ghosted. They cast no glow and no light fill: an
  invisible device casts no visible light (owner's decision).
- seedHiddenBindings(): non-physical devices (excluded domains, Group,
  scene, bridge, myheat children, grouped lamps) in bound areas WITHOUT a
  marker. The editing client materialises them into hidden:true stub
  markers, sets settings.filter_seeded, retires settings.show_all, and
  strips fresh-hidden ids from the red-dot list. Unticking the checkbox
  keeps a hidden:false marker — the seeder never revisits a marked device,
  so the user's decision is final. New non-physical devices hide silently;
  physical ones keep the red-dot flow.
- legacy configs (no filter_seeded) keep the OLD behaviour verbatim —
  runtime filter, shared show_all, hidden-means-gone — until an editing
  client materialises them, so a read-only tablet never sees a half-state.
- 'Show all' is renamed 'Show hidden' and is LOCAL to the tab; the shared
  settings.show_all retires with the runtime filter.
- 'Remove from plan' disappears for auto/entity devices (the checkbox is
  the way); a virtual device's Delete remains a real deletion.
- docs/FILTERING.md is the source of truth for the mechanism.

Tests: seeder/seeded/legacy/lights units (146), smoke_hidden_flag with 12
assertions (68 smokes). Inventory: 146 / 51 / 43 / 68.
2026-07-29 11:35:35 +03:00
Matysh 996a7442ec yellow means working: one principle for the glowing icon
Validate / hacs (push) Failing after 6s
Validate / hassfest (push) Failing after 7s
Validate / frontend (push) Failing after 1m33s
Validate / smoke (push) Skipped
Validate / backend (push) Failing after 6m17s
Research on the owner's install (verified live): the radiator heads that
glowed yellow were the ones with SCALE PROTECTION on, and the ones actually
heating stayed dark. Cause: the primary-entity search ran domains outside
tiers, and switch outranks climate — so a vendor's config switch (anti
scaling, child lock) became the device's primary, driving the color, the
icon morphing and tap-toggle alike.

The principle now: yellow = the device is doing its main job RIGHT NOW.

- primaryEntity: tiers outside, domains inside — a service entity never
  beats the visible main function; a hidden lamp still beats a visible
  config switch (grouped lights), and a plug's switch stays primary.
- climate joins the state table: yellow by hvac_action (heating/cooling/
  drying/fan) — 'which radiators are heating', not 'enabled for winter';
  the coarser state is only a fallback when the integration reports no
  action.
- one truth for light: litLightEntity() is asked by BOTH the glow pool and
  the icon color, in every fill mode — the pool and the icon can no longer
  disagree. The 'is a light source' flag keeps counting controls first.
- README (en+ru): the color table, in words.

Tests: TRV + plug primary units, litLightEntity unit, smoke_yellow_principle
(heating yellow / idle dark / off dark / fallback / lit-light wins / forced
source). Inventory: 142 / 51 / 43 / 67.
2026-07-29 11:07:34 +03:00
Matysh 098a147f87 editor: gestures work on touch — pinch zooms, a moving finger pans
The stage pointerdown bailed out whenever _markup was set, so in the plan
editor no pointer was ever tracked: no pinch, no pan — on a phone the plan
could not be zoomed or moved at all (owner's report). But drawing is
CLICK-based, so the two coexist: a finger that moves pans (and suppresses
the synthesized click so the release feeds no tool), two fingers pinch, a
clean tap still draws. Pointers that start on labels, handles, markers or
buttons stay out — those run their own drags. The tool preview keeps
following the tracked finger.

New smoke: smoke_editor_gestures (pinch in plan mode, pan without drawing,
tap still draws). Inventory: 140 / 51 / 43 / 66.
2026-07-29 10:00:44 +03:00
Matysh 14f7c06bf4 v1.50.4
Validate / hacs (push) Failing after 8s
Validate / hassfest (push) Failing after 10s
Validate / frontend (push) Successful in 1m42s
Validate / backend (push) Failing after 6m26s
Validate / smoke (push) Failing after 11m53s
2026-07-29 08:35:54 +03:00
Matysh 9f36b39379 v1.50.4: one model builder for both cards (HP-1503-01)
The full card's _buildModel() was a hand-copied twin of spaceModels(), and
the twin missed the legacy-store fallbacks v1.50.3 gave the shared builder —
the same broken store rendered recovered in the static card and as
viewBox='0 0 0 0' with negative-width rects in the main one. The divergence
of the duplicates IS the bug, so the duplicate is gone: the full card calls
spaceModels() and only swaps the raw plan url back in (its signing flow must
not bake a signed url into a memoized model — 2026-07-27).

New smoke_legacy_geometry runs the audit's exact vector (zero viewport +
negative rect) through both models and both DOM trees and asserts parity:
full-canvas fallback, normalised rectangle, no negative SVG attributes.
Inventory: 140 / 51 / 43 / 65.
2026-07-29 08:33:10 +03:00
Matysh 9da96abb05 v1.50.3 2026-07-29 08:18:47 +03:00
Matysh df65d25348 v1.50.3: sizes are not coordinates (HP-1502-01)
The ±4 bound from v1.50.2 measured view_box[2:4] and room w/h with the same
ruler as coordinates, so zero and negative sizes still passed the schema —
and viewBox='0 0 0 0' draws nothing on every client, with the static card
computing aspect-ratio: 0 / 0 on top. _EXTENT now requires strictly positive
sizes with a floor of one thousandth of the canvas (1 render unit — far
below any real room, keeps the maths finite); coordinates stay allowed to be
negative, a crop origin legitimately sits past the edge.

Defensive layer for stores that already hold a broken viewport: spaceModels
falls back to the whole canvas — both cards render from that model, so both
get the fallback — and a legacy rectangle with a negative size reads as the
same rectangle drawn from the other corner.

Also: the room settings button is the bottom row of the room card, and the
room name renders in the same spot in view and plan modes (owner's request,
committed earlier on dev).
2026-07-29 08:16:12 +03:00
Matysh 8db6b2673f room card: the name never moves, the settings button sits at the bottom
The label box is centred on the room point, so anything taking part in its
layout SHIFTS THE NAME: entering the plan editor pushed it down by the gear
button's height (owner's report). The name is the anchor now — the metrics
and the gear button hang below it as absolutes, outside the centring math —
so the name renders in exactly the same spot in view mode and in the editor,
and the settings button is the bottom row of the card. Verified by measuring
the name's vb-coordinates in both modes: identical to the pixel.
2026-07-29 08:12:51 +03:00
Matysh c9030af900 v1.50.2 2026-07-29 07:33:01 +03:00
Matysh 5392dadeaa v1.50.2: the v1.50.1 review (HP-1501-01, HP-1501-02)
- HP-1501-01: v1.50.1 bounded layout positions and left room rectangles,
  polygon vertices, view_box and opening coordinates on bare _finite — the
  same absurd-magnitude failure, one schema over. _GEOM (±4) covers them all
  now, opening angles get ±360. And because a store may already hold such a
  vertex from before the door existed, contentBounds applies its canvas
  envelope to room geometry exactly as it does to device positions: the
  point renders where it is, the frame ignores it, a space of nothing but
  absurd points falls back to the whole canvas.
- HP-1501-02: a repair matching zero positions answered ok/moved:0 and
  replaced the one-deep backup with an empty one — a typo right after
  repairing the wrong space destroyed the promised way back. Empty match is
  nothing_to_repair now: no write, no revision bump, backup intact.

Old test fixtures carried view_box [0,0,100,100] from the render-unit days;
they now use the normalised box the product actually stores.
2026-07-29 07:30:22 +03:00
Matysh aa53b33dd6 v1.50.1
Validate / smoke (push) Failing after 13m53s
Validate / hacs (push) Failing after 7s
Validate / hassfest (push) Failing after 8s
Validate / frontend (push) Successful in 1m34s
Validate / backend (push) Failing after 6m46s
2026-07-29 01:41:45 +03:00
Matysh a8ce6020f4 v1.50.1: the v1.50.0 review (HP-1500-01..03)
- HP-1500-02: the stage budget was the absolute document coordinate, so any
  tall dashboard content before the card was billed as header and the stage
  collapsed to 0px. Measure our own chrome relative to the card plus a
  bounded (<=120px) allowance for what the viewport keeps above us; re-measure
  on window resize, remove the listener in disconnectedCallback.
- HP-1500-03, both layers: contentBounds opens a near-zero axis (< ~an icon)
  up to a 200-unit floor and ignores extra points outside a canvas envelope
  (-25%..125%) for FRAMING purposes only; the server bounds layout coordinates
  to +-4 — any finite float used to pass, and one 1e100 hid the plan from
  every viewer. A thin real room keeps its tight frame; the gate sensor past
  the edge still stretches it.
- HP-1500-01: no automatic double-transform — a correct layout and a stranded
  one are indistinguishable, and guessing wrong corrupts good data. Explicit
  admin command houseplan/geometry/repair: dry_run previews, the backup rides
  the same store write, undo restores, and routine layout writes now preserve
  unrelated store keys instead of eating the backup.

Tests: contentBounds guards (unit), layout coordinate bounds + repair
lifecycle (harness), card-below-content smoke. Inventory: 139 / 49 / 42 / 64.
2026-07-29 01:39:10 +03:00
Matysh 9282c28830 v1.50.0 2026-07-28 23:54:16 +03:00
Matysh 8c5d5ba5c5 v1.50.0: the v1.49.0 review (HP-1490-01..04) and the owner's zoom batch
Owner's batch (committed to dev earlier today, released here):
- devices count as content for the default zoom;
- the editor no longer shifts the plan — the stage measures its own top
  instead of assuming 118px of header;
- zoom goes out to 0.4x, centred.

From the review:
- HP-1490-01: the square-canvas migration wrote two stores in sequence, and
  the first write deleted the aspects the second needed — a crash between
  them stranded the layout in the old coordinates with nothing able to
  finish it. The intent {space: old aspect} is durable now: saved to the
  layout store before anything moves, cleared by the same write that stores
  the migrated layout, each half idempotent behind its own trigger. The
  update event fires only after both halves are on disk. Proven at the exact
  crash boundary by a harness test that fails the layout write once.
- HP-1490-02: check_quota and the file write were two executor jobs with
  nothing between them, so N parallel uploads all measured the store before
  any of them wrote. One job under a dedicated upload_lock now — narrower
  than write_lock on purpose, a directory scan must not stall config saves.
  A failed write reserves nothing.
- HP-1490-03: the content frame fed pan, zoom, clamp AND pointer maths, so
  the editors were boxed into yesterday's drawing. Edit modes measure from
  the full square; mode switches refit rather than carry a view clamped
  against the wrong base.
- HP-1490-04: Save could outrun the proportions read and ship the previous
  file's ratio. Picking a plan clears it immediately; Save awaits the
  bounded read and stores 'unknown' over a lie.
- §5: package-lock version synced, duplicated comment removed.

New: smoke_audit_1490.mjs, migration crash-recovery pure + harness tests,
parallel-quota harness test. Inventory: 138 unit / 49 pure / 40 harness / 64
smokes.
2026-07-28 23:50:59 +03:00
Matysh 6c90e03427 zoom-out, device-aware content frame, and the editor no longer shifts the plan
Three owner reports:

- The content frame behind the default zoom only looked at rooms, and devices
  are allowed to stand outside every one of them — a gate sensor by the fence
  was left outside the opening view. contentBounds() takes the marker positions
  now, and they count as content even on a space with no rooms at all.

- Entering an editor 'strangely shifted' the plan. The stage height was
  100dvh minus a hard-coded 118px of header, and the editor header is ~90px
  taller than that: the whole scene slid down by the difference and its bottom
  went below the fold. The card now measures where the stage actually starts
  (HA toolbar, margins and our header included) and gives it the rest of the
  viewport; the measurement is deferred a frame because setting state straight
  from a ResizeObserver callback trips the 'undelivered notifications' error,
  which smoke_dialog_zombie rightly counts as a page error.

- Zoom stopped at the base fit. The floor is 0.4× now, and zoomed out the
  clamp centres the content instead of pinning it to the top-left corner —
  with the view larger than the plan there is nothing to clamp against.

New smoke: smoke_zoom_out.mjs (editor keeps the stage inside the viewport,
0.4 floor, centring, the device-stretched frame); a unit test for the extra
points of contentBounds. Not released — the next release goes out after the
v1.49.0 audit.
2026-07-28 23:40:39 +03:00
Matysh 9b180c5917 changelog: describe the plan check as it ended up (new references only) 2026-07-28 22:54:00 +03:00
Matysh 3084472c75 v1.49.0 2026-07-28 22:53:35 +03:00
Matysh c00048611e HP-1470-02: only refuse a plan reference that is NEW and already broken
CI caught what the local pure suite cannot run. Four HA-harness tests store a
plan url whose file is not there — and so, sooner or later, will a user: files
disappear from outside Home Assistant, and one of them is what the 'broken plan'
repair exists to report. Refusing every write that names a missing file would
have locked the owner out of every edit, including detaching it.

So the check compares against the stored configuration and only refuses names it
has not seen before, which is exactly the pick-then-delete window it was written
for. The repairs test now attaches a real plan and removes the file behind it;
the quota test budgets from what the shared test config directory already holds
instead of assuming an empty folder.
2026-07-28 22:51:07 +03:00
Matysh f5e6c0318d v1.49.0: content-fit zoom, swipe animation, wording, and the v1.47.0 review
Owner's batch:
- zoom now opens on what is DRAWN (rooms + 5% margin) for spaces with no
  background image; with one the image is the plan and still fits whole. A small
  plan on the square canvas no longer opens as a speck.
- swiping between spaces, and the kiosk carousel, slide sideways; honours
  prefers-reduced-motion.
- the room settings button reads 'Room settings' and lightens on hover.
- 'curation' is filtering everywhere: UI strings, docs, code.

Checked the yard while I was there: its drawing sits off-centre because it was
drawn that way — before the migration x spanned 0.12..0.54 with 0.12 and 0.46 of
margin. The migration added 0.1465 on each side, symmetrically. Content-fit zoom
makes it moot anyway.

From the v1.47.0 review:
- HP-1470-02: the picker let you delete the plan you had just selected — it is
  not in the stored config yet, so the server rightly called it free, and the
  save then stored a url with no file. The button is disabled, and since two
  clients can do this in either order, config/set now verifies every internal
  plan url against the disk under the write lock and answers .
  External and legacy urls are not ours to police.
- HP-1470-01: growth is bounded at the door rather than by deleting old files —
  that mistake cost real plans twice. check_quota refuses an upload that would
  push the store past 256 MB / 200 plans (1 GB / 1000 attachments) or leave less
  than 512 MB free. The plan list is capped at 60 newest with a total, and
  thumbnails load lazily.
- HP-1470-03: picking a saved plan waited for nothing and stored a fallback
  ratio when the signature had not arrived — a square plan came out stretched.
  It waits for the signature, binds the result to the dialog that asked, and the
  dialog preview is signed too.
- report §5: the last lifecycle comments still described age-based collection.

Not released yet — the owner asked for a release once the batch is done.
2026-07-28 22:44:09 +03:00
Matysh e1e730560d fix: the migrated viewport must be the whole square, not the old rectangle
Seen on the live instance: in the editors the dot grid covered only part of the
canvas. The grid is drawn over the space's view_box, and I transformed that box
along with everything else — so it still described the old plan area, and the
margins the square canvas had just added were outside it. Nothing to draw on,
which is precisely the room the change exists to give.

The viewport is now reset to the full square. It is also what 'fit to screen'
fits, so the whole canvas is reachable.
2026-07-28 22:28:40 +03:00
Matysh 94b298962a v1.48.0: the canvas is always square, the plan is centred inside it
A space carried an aspect ratio, and coordinates were normalised against it: x
by the width, y by the height. Every geometric question therefore depended on a
per-space number, and picking a canvas orientation was a decision the user had
no reason to make. The render space is now NORM_W x NORM_W and a plan image is
fitted into it by its OWN ratio, centred — wide plans get margins above and
below, tall ones at the sides.

Migration (geometry_migration.py, pure and unit-tested) runs once at setup under
the write lock. Nothing about a drawing changes: the old box is padded out to a
square and every coordinate re-expressed against it — rooms as rects and
polygons, openings and their lengths, decor, view_box, and the marker positions
in the separate layout store. In render units it is a uniform scale plus an
offset, so angles and proportions are exact. cell_cm is scaled for tall plans,
because the grid pitch is a fraction of the width: without it a wall would
measure less than it does.

 is now dropped by the schema rather than accepted — a stale tab sending
it would be sending coordinates from the old normalisation too, and honouring
the field would not make them right.

The demo fixture was migrated with the same transform, so the smokes exercise
the new geometry rather than a square-native fake; six of them needed their
render-space helpers updated and one its click coordinates.

Not released — dev only, per the owner's instruction.
2026-07-28 22:20:59 +03:00
Matysh f7fe63776a Release v1.47.0
Pick a plan you already uploaded: the space dialog lists the plans stored on the
server, attaches one on click, and is the only place a plan file is deleted.
2026-07-28 21:55:24 +03:00
Matysh 01bc4f9711 test: the plans folder is shared across the module
Assert on our own two files rather than the whole listing.
2026-07-28 21:52:12 +03:00
Matysh 85491d0fea v1.47.0: pick a plan you already uploaded
Closes both findings from the v1.46.6 review with one feature, because they are
the same gap seen from two sides. HP-1466-02: a detached plan stayed on disk and
could not be re-attached from the card — the old url is nowhere in the config,
and the backend test 'proved' reattach by remembering it in a Python variable.
HP-1466-01: files kept forever with no way to see or remove them is not a
policy, it is accumulation.

New: houseplan/plans/list (name, url, size, modified, and which spaces use it)
and houseplan/plans/delete, which refuses while a space still references the
file — the stored configuration answers that, not the client. In the space
dialog, 'Already uploaded' shows the list with thumbnails; one click attaches,
reading the aspect from the image as an upload does; the trash button is the
only way a plan file is ever deleted.

That also bounds the disk without any timer, which is the part every automatic
attempt got wrong: v1.46.4 deleted detached plans, v1.46.5 raced the retry that
was about to reference an upload. The user decides, and can now see what they
are deciding about.

Docs: comments in plans.py and websocket_api.py still described the age-based
collection v1.46.6 removed (report §6); ARCHITECTURE gained the two new routes
and an explanation of why the listing is what makes 'never delete' livable.
2026-07-28 21:49:37 +03:00
Matysh d37a67c29f Release v1.46.6
Detaching a plan finally keeps the file where it matters — at the save, not just
on the scheduled pass. Transitions are classified by the space that owned the
file, and nothing is deleted for being old except a staging folder.
2026-07-28 21:25:50 +03:00
Matysh a66272c6f4 test: two HA-harness tests still asserted the old age rule
One demanded an aged upload be collected; the shared sweep fixture expected an
aged plan file to disappear. Both now assert the opposite, which is the rule.
2026-07-28 21:22:33 +03:00
Matysh f4af2fe508 fix: stop ageing files out entirely, except staging folders
The strengthened race test earned its keep on the first run: the sweep deleted
an aged 'rejected upload' while a save was committing a reference to it, and the
accepted config came out pointing at nothing. The write lock serializes the two
but cannot help when the sweep goes first.

So the age rule is gone for plans and for marker folders. What remains is one
sentence: a file goes when an action says so — a plan replaced, an attachment
dropped from a device that still exists — plus a per-dialog staging folder after
an hour, which by construction can only hold an upload nobody saved.

Cost: an upload whose save failed sits there until someone removes it by hand.
That is the side of the trade the owner picked, and it is the side that cannot
lose data.
2026-07-28 21:18:53 +03:00
Matysh 8e07e3c958 test: race the sweep against a save, not a reload against a save
A reload has an unload window where any WS call answers not_ready, so the save
failed at random — and the vaguer assertion this test used to carry was exactly
what hid that. Driving data.sweep() directly is the concurrency the write lock
actually guards.
2026-07-28 21:13:45 +03:00
Matysh 9868f1035f v1.46.6: the detach promise, actually kept this time
v1.46.4 and v1.46.5 documented that detaching a plan leaves the image on disk,
added guards for it, and shipped tests. The guards were never reached: they sit
behind 'not superseded', and a file that left the configuration was called
superseded. From old_refs - new_refs alone, replacing a plan, detaching one and
deleting its space are indistinguishable — so all three deleted the file, at the
moment of the save, before any scheduled pass ever ran.

Every test I wrote for this called collect_plans(d, cfg, cfg): old config equal
to new, i.e. only the scheduled pass. The transition that mattered was never
exercised. Codex reproduced it in four lines.

Classification is by owner now:
  space in both, plan A -> plan B  : the user picked another image -> removed
  space in both, plan -> none      : detached -> kept
  space gone                       : kept (the image was imported; a thirty-day
                                     grace measured from file age is meaningless
                                     anyway, it was uploaded months ago)
  space has a plan, other file     : rejected upload -> 1 h
Attachments follow the same shape: dropped from a device that still exists ->
removed (a trash button promises nothing); device gone -> kept; staging folder
-> 1 h.

Tests: a matrix per rule in the pure module, and — the part that was missing —
test_detaching_a_plan_keeps_the_file, which goes through real config/set calls:
attach, detach, assert the file is there, restart, assert again, re-attach,
replace, assert the replaced one is gone, delete the space, assert the plan
survives. Also strengthened the sweep/save race test to assert the save actually
succeeded and the config points at the specific expected file, per the report.
2026-07-28 21:11:11 +03:00
Matysh f2c9b07cc1 Release v1.46.5
Audit of every automatic deletion: a detached plan is never removed (standing
rule now in SCOPE.md), files/cleanup verifies against the stored config instead
of trusting the client, and a deleted space's plan waits thirty days.
2026-07-28 20:41:06 +03:00
Matysh 2c7a2f849d test: files/cleanup reports counts now, not a boolean
It answers {removed, kept} since v1.46.5 — the 'kept' side is the point: files
the stored configuration still references survive a cleanup of their folder.
2026-07-28 20:38:49 +03:00
Matysh 33e71ca96c v1.46.5: audit of every automatic deletion
Owner's decision after the incident: a detached plan is never deleted, at any
age. v1.46.4 gave it a month; this makes it permanent and, more importantly,
writes the reasoning where the next change will trip over it — docs/SCOPE.md now
carries the standing rule. The component may delete a file only when a user
action says so. 'Nothing points at this any more' is not such an action, because
the two errors are not symmetrical: wasted disk is visible, cheap and
reversible; a deleted file is none of those.

Went through every other automatic deletion with the same question. One more
was wrong: houseplan/files/cleanup rmtree'd whatever folder the card named. A
partial migration leaves urls pointing into it — files/migrate deliberately does
not rewrite the ones it could not confirm — so those were live links to files
being deleted; and a wrong or stale id from any client destroyed a live device's
manuals. The server now reads the stored config under its lock and removes only
what nothing references, keeping the rest and saying so.

Also: a plan of a DELETED space now waits thirty days rather than an hour.
Deleting a space is deliberate; an hour is a short window to notice a misclick.

The rest came out clean: layout/delete and marker/room/space removal are all
confirm-guarded user actions, upload temporaries are never user-visible, and
dropping legacy 'segments' is a documented migration.
2026-07-28 20:36:17 +03:00
Matysh 7128ab504d Release v1.46.4
Data loss fix: collection treated a detached plan as abandoned and removed it
after an hour. Supersession stays immediate; absence is now judged per case.
2026-07-28 20:04:07 +03:00
Matysh f953a3c286 fix: the guard has to be per-case, not blanket
Protecting every file of a live space also protected the ones a commit had just
superseded, and gave rejected uploads immortality. The distinction that matters
is narrower: a space with NO plan_url has had its image detached and may want it
back; a space that has one can only be holding its own rejects. Attachments:
staging folders keep the hour, marker folders get the month.

Also: the layout event test asserted the order of separately fired bus events,
which nothing promises — it came back [2,1,3] in CI.
2026-07-28 19:59:32 +03:00
Matysh ef270d11b7 v1.46.4: detached plans were being collected as garbage — data loss
Deployed v1.46.3 to my own instance, restarted, and the startup sweep deleted
both floor plans: config/houseplan/plans/ went from f1.svg + f2.png to empty.
The backup is a SecureTar, so they are gone.

The rule was wrong, not the code. v1.46.0 introduced collection that treats
'nothing references this right now' as abandoned and gives it an hour. But
detaching a plan — switching a space to 'draw' — is a normal, reversible action,
and the editor's own comment says the file stays on disk. Those two plans had
been detached for weeks; every pass since v1.46.0 was entitled to remove them,
and the one that finally ran did.

New rule, one for every path:
  * superseded by a commit (was in the old revision, is not in the new) — goes
    immediately; that is the one thing a commit knows for certain;
  * belongs to a space or marker that still exists — never collected, at any
    age, because unreferenced is not abandoned;
  * a per-dialog staging folder (up_*) — one hour, unchanged: by construction it
    only ever holds an upload from a dialog that was never saved;
  * anything else — thirty days.

The  flag I added an hour ago is gone with it: two rules for the same
question is how this happened. Tests updated to the new grace, plus two that pin
the distinction directly.

I am sorry about the files.
2026-07-28 19:55:38 +03:00
Matysh dc24390222 Release v1.46.3
Re-check of v1.46.2: the startup sweep uses the runtime data it already has
instead of a lookup that cannot succeed during setup, and the test that was
supposed to prove it no longer passes for the wrong reason.
2026-07-28 19:45:39 +03:00
Matysh 254354bf56 test: invoke the scheduled sweep instead of faking a 24 h jump
The time-changed variant failed in CI: the timer fired but the assertion still
saw the orphan, and 'the timer fires' and 'the work happens' are different
claims anyway. HouseplanData now publishes the sweep, so the test awaits it
directly and asserts the outcome.
2026-07-28 19:35:09 +03:00
Matysh c9a60a110d chore: untrack __pycache__
.gitignore has covered it for a long time, but four .pyc files were committed
before the rule existed and kept turning up in every diff.
2026-07-28 19:31:53 +03:00
Matysh 75279308c1 v1.46.3: re-check of v1.46.2 — HP-1462-01
The startup sweep resolved its runtime data with get_data(hass), which lists
only LOADED entries — during async_setup_entry the entry is still
SETUP_IN_PROGRESS, so it always got None and degraded to removing streaming
temporaries. The real collection was then 24 hours away, and an instance that
restarts more often than that never ran it at all. It closes over the
object created a few lines above instead; the callback is unregistered with the
entry, so that matches the lifecycle.

The test that was meant to prove the previous fix passed for the wrong reason:
it seeded the strays BEFORE config/set, which collects too, so nothing was left
for the restart to find. Now seeded after the save, plus two more — one firing
the interval callback on its own, and one running a reload and a save
concurrently to assert the accepted config never references a file the sweep
removed (they share the write lock; this pins that they must).
Docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
2026-07-28 19:31:29 +03:00
Matysh b7ae3e7adf Release v1.46.2
Validate / hacs (push) Failing after 7s
Validate / hassfest (push) Failing after 7s
Validate / frontend (push) Successful in 1m51s
Validate / backend (push) Failing after 7m11s
Validate / smoke (push) Failing after 6m35s
Re-check of v1.46.1: the scheduled sweep now collects unreferenced attachments
and plans against the stored configuration, and a drag in flight survives a
concurrent remote position change.
2026-07-28 17:47:19 +03:00
Matysh 379fb68db2 v1.46.2: re-check of v1.46.1 — HP-1461-01, -02
Validate / hacs (push) Failing after 23s
Validate / hassfest (push) Failing after 22s
Validate / frontend (push) Successful in 1m40s
Validate / backend (push) Failing after 7m16s
Validate / smoke (push) Failing after 7m13s
HP-1461-01: collection was tied to config/set, which is the right scope for
what a commit supersedes but leaves a file nobody references with no future
write to notice it — cancel a dialog after the upload finished, drop the
connection just after, or call the upload API directly. The daily sweep added
in v1.46.1 only removed streaming temporaries, so the documented 'a cancelled
attachment is collected an hour later' did not hold on an instance nobody
edits. The scheduled pass now loads the stored config under the same write_lock
a commit uses and runs collect_attachments/collect_plans with it as BOTH sides:
nothing counts as superseded, referenced files are preserved, aged unreferenced
ones go. Doing it under the lock keeps it from deciding on a snapshot a commit
is about to replace.

HP-1461-02: _reloadLayoutOnly captured the dirty set AFTER flushing the pending
write, and the flush empties it first — so during a real drag (where a write is
already scheduled) the snapshot was empty and the server's older position was
merged over the user's move. The snapshot is taken before the flush, by value,
and a _sentPos map now holds positions that are sent but unacknowledged, which
closes the same window for a write that was already in flight.

Tests: the upload test now cancels the request task for real (the previous one
claimed to and only walked error paths); smoke_layout_sync schedules a genuine
debounced write and delays it — verified failing on a v1.46.1 build with
exactly the reported symptom; a new backend test reloads the entry and asserts
the scheduled sweep takes an aged cancelled attachment and an orphan plan while
keeping everything the config still references.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
2026-07-28 17:44:03 +03:00
Matysh 96a70495d3 Release v1.46.1
Re-check of v1.46.0: atomic filename reservation with a bounded collision name,
unconditional cleanup of streaming temporaries plus a scheduled sweep, and the
full card following layout events with a dirty-position merge.
2026-07-28 16:51:19 +03:00
Matysh d3db9e30e6 v1.46.1: re-check of v1.46.0 — HP-1460-01, -02, -03
HP-1460-01: v1.46.0 stopped overwriting attachments, but picking a free name
and taking it were two steps. Two uploads racing between them agreed on the
same name, both answered 200, and one set of bytes replaced the other;
files/migrate had the same check-then-copy gap. reserve_filename now claims the
name with O_CREAT|O_EXCL as it picks it, and both paths use it. It also splits
the extension off the RAW name and budgets the stem against MAX_FILENAME
including the collision tag — a maximal name lost its '.pdf' and then grew past
the limit, so the view sanitised the request back to a different name and the
attachment 404'd for good.

HP-1460-02: cleanup lived in an 'except Exception', which CancelledError walks
past, only one tmp_path was tracked, promotion had no finally, and the
collector only walks marker folders — an aborted transfer stranded a .upload-*
that nothing would ever remove. An outer finally owns every temporary, a second
'file' part is refused, promotion failure cleans up, and sweep_upload_temps
runs at setup, daily, and inside the commit-scoped collector. Chunks are
batched to 1 MB per disk task instead of one per 64 KB.

HP-1460-03: the layout event reached the static card and not the full one, so
two full cards diverged until a reload. The full card subscribes now and
re-reads ONLY the layout, keyed on its revision. Two hazards handled: it
records revisions it produced itself, and the reaction is deferred ~200 ms
because the event can beat the reply to our own write over the same socket;
positions dragged but not yet sent are flushed and merged on top, so a fix for
a stale UI cannot become a lost drag.

Tests: smoke_layout_sync (fails on a v1.46.0 build), four pure tests for atomic
reservation incl. 20-thread concurrency and the length boundary, a backend test
walking every failing exit path of an upload, and — as the report asked — an
HA-harness test that a repair issue disappears with its space.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
2026-07-28 16:48:32 +03:00
Matysh 2e731debd9 Release v1.46.0
Full external audit of v1.45.4: sandboxed SVG content (release blocker),
transactional attachments, serialized config writes, geometry-aware openPairs
cache, inner validation limits, streaming file I/O, static-card parity, layout
revisions and events, repair issue cleanup, dev dependency bump.
2026-07-28 16:18:58 +03:00
Matysh a49b5e6d2e fix: collision names must survive the sanitiser the content view applies
unique_filename produced 'manual (2).pdf'; HouseplanContentView sanitises the
name in the REQUEST too, turning ' (2)' into '_2_', so the file was written and
then 404'd. The same pattern was already in files/migrate, so a rebind that hit
a name collision has been producing dead links. Both use the shared helper now,
with '-2', which round-trips sanitize_filename — asserted.
2026-07-28 16:16:07 +03:00
Matysh 4418312b0b test: config cap under aiohttp's 4 MB frame; own marker id for the upload test
A 4 MB cap could never be reported: the frame limit rejects the message first
and the socket closes with 1009, so the user gets a dropped connection instead
of 'too_large'. 2 MB is ~30x a real three-floor configuration (70 KB measured).

The upload test listed a folder test_ha_upload.py also writes into.
2026-07-28 16:11:48 +03:00
Matysh 3f719cc32a test: match the new upload url shape; keep the config cap under the WS frame limit
test_upload_ok still asserted the old '<name>?v=<mtime>' url — uploads take a
free name now, so the name itself is the cache key and the query is gone.

MAX_CONFIG_BYTES was 12 MB, above the WebSocket frame limit: a payload that big
never reaches the handler, the socket just closes with 1009 and the user sees a
dropped connection instead of an actionable error. 4 MB is far above any real
configuration and comfortably inside the frame.

test_upload_never_overwrites listed the whole shared test config folder.
2026-07-28 16:09:00 +03:00
Matysh 260615a63f v1.46.0: full external audit of v1.45.4 — HP-1454-01 … -10
HP-1454-01 (high, release blocker): an uploaded SVG plan opened directly is a
top-level document of Home Assistant's own origin, so a <script> inside it
reaches the session's localStorage and API. Uploading needs write access, which
by default every authenticated user has. SVG responses now carry a sandbox CSP;
only SVG, because a CSP on a PDF can break the browser's viewer and a raster
image has nothing to disable. Verified in Chromium both ways: the script runs
without the header and does not with it.

HP-1454-02: attachment uploads wrote straight to <marker>/<filename>, outside
the config transaction — a cancelled dialog or a rejected save left the stored
url serving new bytes, and every new icon shared one 'new' folder, so two of
them attaching manual.pdf pointed at one file. Uploads take a free name, a new
icon gets a per-dialog staging folder promoted on an accepted save, and
config/set collects superseded and aged-orphan attachments like it does plans.

HP-1454-03: the debounce spaced out the starts of a write, not the writes. A
save slower than 500 ms let the next edit go out with the same expected_rev;
the server accepted the first, rejected the second, and the conflict handler
reloaded over the local copy. Writes are chained now — one in flight, each with
the revision the previous returned.

HP-1454-04: _openPairsCache keyed on room ids and links only, so an aspect
change or a dragged vertex left open boundaries and their glow cuts at old
coordinates. It keys on the rendered model object now — the same invalidation
the model cache already has, not a second strategy. The fingerprint also gained
an O(1) geometry roll-up per room.

HP-1454-05: outer collections were capped, inner ones were not. Limits for
poly points, open_to, controls, pdfs, text and url lengths, plus a total
serialized size cap; legacy  is dropped server-side.

HP-1454-06: upload streams to a temp file and downloads use FileResponse, so a
50 MB manual no longer costs ~100 MB of RSS per transfer.

HP-1454-07: spaceModels() dropped room.settings, so the static card ignored the
per-room fill override. HP-1454-08: layout had no revision on point-wise writes
and no event, leaving static cards stale forever; it now keeps a revision,
returns it and fires houseplan_layout_updated. HP-1454-09: repair cleanup only
walked existing spaces, so a deleted space kept its warning. HP-1454-10:
serialize-javascript pinned past two advisories.

Tests: smoke_svg_sandbox (proves both directions), smoke_config_writer and
smoke_render_parity (both verified failing against a v1.45.4 build), six pure
tests for attachment collection and inner limits, four HA-harness tests for the
CSP, non-overwriting uploads, the size cap and layout revisions.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
2026-07-28 16:06:21 +03:00
Matysh e4e300adaa Release v1.45.4
Validate / hacs (push) Failing after 1m19s
Validate / hassfest (push) Failing after 1m18s
Validate / frontend (push) Successful in 2m13s
Validate / backend (push) Failing after 10m58s
Validate / smoke (push) Failing after 6m7s
Review of v1.45.3: R5-1 a partial signing answer no longer skips the backoff,
R5-2 the status snapshot matches the repository and no longer carries counts
that go stale.
2026-07-28 08:51:57 +03:00
Matysh 96d387ff1d v1.45.4: review of v1.45.3 — R5-1, R5-2
Validate / hassfest (push) Failing after 49s
Validate / hacs (push) Failing after 52s
Validate / frontend (push) Successful in 1m43s
Validate / backend (push) Failing after 8m30s
Validate / smoke (push) Successful in 4m52s
R5-1: the backend signs each path independently and answers successfully with
whatever it managed, skipping (and logging) the rest. The card read any
successful call as 'the batch is done', cleared the backoff for every path in
it, then wrote only the urls that came back — so a path the backend kept
skipping was asked for again on every render, the exact amplification the
backoff was added to stop. A path now counts as signed only when the answer
carries a url for it; the others back off individually, keys that were not
requested are ignored, and onUpdate fires only when a new signature landed.

R5-2: docs/STATUS.md still described main as holding releases up to v1.40.1 and
quoted test counts several releases old, while the version line beside them was
kept current — a handoff reader got a wrong branch model and less coverage than
exists. Branch roles are now accurate, and the counts are gone rather than
corrected: scripts/inventory.mjs (npm run inventory) prints them from the tree,
so there is nothing left to drift.

Tests: three unit cases for empty/partial/foreign-key answers, verified to fail
against a v1.45.3 checkout; a backend test pinning the partial-success contract
by making async_sign_path raise for one path of two.
Docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
2026-07-28 08:49:11 +03:00
Matysh 8b531db3f5 docs: the value-display bug lived six days, not a year and a half
Validate / hacs (push) Failing after 7s
Validate / hassfest (push) Failing after 6s
Validate / frontend (push) Successful in 1m44s
Validate / backend (push) Failing after 6m15s
Validate / smoke (push) Failing after 12m28s
Version distance is not calendar distance. v1.26.0 shipped 2026-07-21 and the
report came in on 2026-07-27; the project itself is three weeks old. The point
stands and is unchanged — nothing in the suite could have caught it, because the
option list and the schema were written in two languages and never compared —
but the 'year and a half' was wrong.
2026-07-28 00:29:40 +03:00
Matysh 68aa1f04ba Release v1.45.3
Validate / hacs (push) Failing after 5s
Validate / hassfest (push) Failing after 5s
Validate / frontend (push) Successful in 1m34s
Validate / backend (push) Failing after 6m18s
Validate / smoke (push) Failing after 10m27s
issue #3: display='value' was offered by the editor since v1.26.0 but rejected
by the schema, which blocked saving the configuration entirely. Option lists
are now shared and checked across languages.
2026-07-28 00:26:27 +03:00
Matysh 3d41fe16b8 v1.45.3: 'value instead of an icon' could never be saved (issue #3)
The device editor has offered display='value' since v1.26.0; MARKER_SCHEMA
accepted only badge/ripple/icon_ripple. Picking it produced

  not a valid value for dictionary value @ data['config']['markers'][n]['display']

and since one rejected marker fails the whole config write, the user could not
save the plan at all until the setting was undone. Reported by @RemyRoux with
the exact error text, 2026-07-27 — a year and a half after the feature shipped.

The schema now accepts it, and the class of bug is closed rather than the
instance: DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_MODES and ROOM_FILL_MODES are
exported from src/logic.ts, the editors render their options from them, and a
backend test parses those lists out of the TypeScript source and asserts the
schema accepts every one (and rejects a bogus value). Reverting the one-word
schema fix fails that test, which is the check that was missing.

Plus an HA-harness test saving a config that contains a value-display marker —
the exact call the user's card was making.
Docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
2026-07-28 00:23:37 +03:00
Matysh ac734688d4 Release v1.45.2
Hardening from the v1.45.1 review: R4-1 a failed cleanup no longer reports an
accepted save as an error, R4-2 one signing request per url instead of one per
render.
2026-07-28 00:16:34 +03:00
Matysh 2e2d353b04 v1.45.2: hardening from the v1.45.1 review — R4-1, R4-2
R4-1: collecting superseded plan files runs after the configuration is already
durable, but an error listing the directory propagated out of config/set. The
client saw a failure for a revision the server had committed, and its retry
came back as a conflict. collect_plans now reports 0 instead of raising, and
config/set logs and proceeds — the event fires, the revision is returned.

R4-2: the pending set was cleared when a batch went out, not when it came back,
so every render during an in-flight content/sign queued another request: six
calls where one was needed, and unbounded on a socket that is slow rather than
busy. Queued and in-flight are separate states now; a failure backs off (2 s
doubling to 60 s) instead of retrying on the next frame; an in-flight entry
expires after 15 s so a promise that never settles cannot wedge retries; a late
answer after dispose() no longer renders.

Tests: test/signing.test.mjs — eight cases with hand-settled promises, verified
against a v1.45.1 checkout where four of them fail (2 sign calls instead of 1,
no backoff, a late answer rendering after teardown). Backend: a broken
collector still yields a successful save whose revision the next CAS accepts.
Pure collector: a disappearing directory returns 0.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
2026-07-28 00:13:45 +03:00
Matysh f8c8cb4eeb Release v1.45.1
Follow-up review of v1.45.0: R3-1 plan collection moved into the config
transaction, R3-2 the static space card now uses the signed background url.
2026-07-27 22:04:22 +03:00
Matysh c749b52a0d v1.45.1: follow-up review of v1.45.0 — R3-1, R3-2
R3-1 (high): v1.45.0 made the upload safe but left deletion to the client —
after a successful save the card asked the backend to remove everything but the
file it had just committed. Two open editors cannot be ordered: a delayed
request from one deleted the plan the other had just saved, leaving the
accepted configuration pointing at nothing, the exact damage copy-on-write was
introduced to prevent.

houseplan/plan/cleanup is removed. config/set collects inside its own write
lock from the two configurations that bracket the commit (plans.collect_plans):
a file the old revision referenced and the new one does not is superseded and
goes; any other unreferenced upload waits out PLAN_ORPHAN_TTL_S, because a
fresh one may belong to a transaction that has not committed yet. The collector
lives in a pure module so it can be reasoned about and unit-tested without the
HA harness.

R3-2: houseplan-space-card signed its plan url and threw the result away —
getCardSize() mutated a throwaway model while render() rebuilt its own from the
config, so the <image> requested the protected path and got 401 on every
render. Both cards now share ContentSigner (src/signing.ts), which also gives
the static card batching, expiry handling and periodic re-signing.  is
released in finally: one failed request no longer wedges a url for the life of
the page.

Tests: five backend interleaving cases from the report, six unit tests for the
pure collector, smoke_space_card_bg (verified to fail against a v1.45.0 build:
the raw url reaches the DOM and no retry happens). 57 smokes, 124 unit, 22
backend-pure.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
2026-07-27 22:01:41 +03:00
Matysh 15e5dd7392 Release v1.45.0
External review of v1.44.8: R2-1 plan upload transaction boundary,
R2-2 signed-url batching and expiry, R2-3 room climate in one registry pass.
2026-07-27 21:14:42 +03:00
Matysh f1b501a956 test: isolate the plan-upload transaction test from a shared config dir
The HA harness reuses one config directory inside a module, so the s1 upload
left by test_plan_set_validates counted as a third file and the cleanup
assertion read 3 instead of 2. Own space id plus a defensive sweep.
2026-07-27 21:11:32 +03:00
Matysh 5d2dbb1009 v1.45.0: external review of v1.44.8 — R2-1, R2-2, R2-3
R2-1 (high): plan replacement committed filesystem state before the config CAS.
The upload wrote the final name and unlinked the other extension, so a rejected
config write left the live plan already replaced — or the stored config
pointing at a deleted file. Uploads now go to <space>.<token>.<ext> and delete
nothing; houseplan/plan/cleanup runs only after the config write is accepted.
The '.' separator is load-bearing: a space id cannot contain one, so cleaning
'f1' can never reach the files of 'f1-attic'.

R2-2: the backend signs at most MAX_SIGN_PATHS (200) per request and ignores
the rest silently, while the card sent its whole cache in one call and trusted
any cached entry forever — past 200 attachments the later ones stopped being
refreshed and expired for good. Requests are chunked to the shared constant,
entries carry their issue time (aging urls keep rendering while a replacement
is fetched, expired ones are dropped), and the cache is pruned to urls the live
config still references.

R2-3: areaClimate() rescanned the whole registry per room and per measurement.
areaClimateMap() classifies once and returns Map<area,{temp,hum}>, memoized on
hass identity so fresh states are always observed. Smoke measurement: 133
registry scans per update with 44 rooms before, 2 after, flat in room count.

Also: smoke_ux_fixes wrote its screenshot to a hard-coded /tmp path and could
not run on Windows.

Tests: smoke_plan_upload_reject, smoke_sign_cap, smoke_climate_once (all fail
on v1.44.8), three backend tests for versioned plan names and cleanup scoping,
unit tests for chunk/referencedContentUrls and areaClimateMap.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
2026-07-27 21:08:34 +03:00
Matysh 40cb0302e3 Release v1.44.8
Validate / hacs (push) Failing after 7s
Validate / hassfest (push) Failing after 6s
Validate / frontend (push) Successful in 1m23s
Validate / backend (push) Failing after 6m59s
Validate / smoke (push) Successful in 7m26s
v1.44.6 room climate counts only air temperature
v1.44.7 plan backgrounds never displayed (signed-url regression)
v1.44.8 an uploaded plan never reached the config
2026-07-27 15:36:09 +03:00
Matysh 14cc4df4bd chore: sync the committed card bundle with dist (v1.44.8)
Validate / hacs (push) Failing after 11s
Validate / hassfest (push) Failing after 9s
Validate / frontend (push) Successful in 1m36s
Validate / backend (push) Failing after 6m41s
Validate / smoke (push) Failing after 37s
CI checks `cmp dist == custom_components/houseplan/frontend`; the three
previous commits shipped source and docs without the rebuilt bundle, so
validate.yml failed on all of them. Same folder that HA serves statically —
the one that must never be skipped.
2026-07-27 15:33:21 +03:00
Matysh ead56dd9b6 v1.44.8: an uploaded plan never reached the config
Found on the owner's install: the image lands in /config/houseplan/plans, the
space keeps plan_url=null, the plan never shows and re-saving does not help.

_saveSpaceDialog held a reference to the space object across the await that
uploads the file. _reloadConfigOnly() — which runs on every
houseplan_config_updated event — REPLACES _serverCfg, so that reference became
an orphan: plan_url, aspect, title and every display setting were written into a
detached object while the save shipped the untouched config. In 'create' mode
the whole new space was lost the same way.

- upload first, then touch the config; no reference is held across an await.
- _saveConfigNow() sets _cfgWriting like the debounced writer, so a revision
  arriving mid-save defers its reload instead of replacing the config (audit L2
  extended to this path).
- demo/smoke_plan_upload_race.mjs: on v1.44.7 the sent config still carries the
  OLD plan_url and the created space is missing; passes here. The demo's
  config/get now returns a fresh object, as a real server does — returning the
  same reference is what hid this class of bug from the smoke layer.
- DEVELOPMENT.md: the deploy target is custom_components/houseplan/frontend/,
  and deploy verification must go over HTTP. A copy placed next to __init__.py
  is served by nobody — that cost two deployments today.
- docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
2026-07-27 15:14:19 +03:00
Matysh 018b37940f v1.44.7: plan backgrounds never displayed (regression from v1.44.5)
The card signs content urls because a browser cannot authenticate an <image
href>. But _display() was called inside _buildModel(), and the space model is
memoized on the config fingerprint — so the UNSIGNED url froze in the cache and
the signature, which did arrive, never reached the element. The plan never
loaded, and the browser kept hitting the unsigned path: 401, which Home
Assistant reports as a failed login attempt from the viewer's own IP (that is
how the owner spotted it). PDF links were unaffected: they already resolved at
render time.

- _buildModel() keeps the raw plan_url; the render pass calls _display().
- _display() returns '' for an unsigned content url instead of the plain path,
  and the <image> is not emitted at all until the signature lands — no 401, no
  spurious login-attempt warning.
- _resign() replaces 'drop everything and re-request': the previous urls are
  kept until the new ones arrive, so a wall tablet never blanks.
- demo/smoke_plan_signed.mjs: reproduces on v1.44.6 (href stays ?v=..., never
  ?authSig=), passes here. TESTING.md row added.
- docs: CHANGELOG.md + CHANGELOG.ru.md + STATUS.md.
2026-07-27 15:05:46 +03:00
Matysh ebeaa5c0c6 v1.44.6: room climate counts only air temperature
After v1.44.5 read the area registry instead of visible icons, every hidden
temperature entity in the area became a candidate, including ones measuring
something other than room air. Verified against a live 60-area install: a NAS
processor temperature, kettle water, a 90 C sauna heater and a virtual
better_thermostat all leaked into room averages.

- areaClimate(): skip entity_category (diagnostic/config), skip EXCLUDED_DOMAINS
  platforms, skip entity ids naming a non-air medium (water/coolant/flow_temp/
  return_temp/target/setpoint/chip/cpu/processor/board/device_temp/batter/
  freezer/fridge/oven/kettle/boiler).
- rules.ts: kettle/thermopot -> mdi:kettle, sauna/harvia -> mdi:hot-tub, so they
  no longer fall through to the generic thermometer rule.
- test: all four real false positives asserted out, one real sensor left.
- docs: CHANGELOG.md + CHANGELOG.ru.md + STATUS.md snapshot.
2026-07-27 14:38:50 +03:00
Matysh 02ba18dc7b Merge dev: v1.44.3..v1.44.5 (B1 regression fix, audit follow-up, room climate) 2026-07-27 14:24:46 +03:00
Matysh 715a93ec61 fix v1.44.5: room climate counts hidden sensors; drop the stale room tooltip
- areaClimate() walks the HA registry for the area instead of the list
  of VISIBLE icons: a thermometer hidden by curation or by the user was
  silently dropped from the room card, tooltip and temperature fill
  (field report). Curation still filters fridges/TRVs; the auto icon is
  used on purpose so a custom marker icon cannot change what a device
  measures; an explicit per-room source still wins
- room tooltip no longer says 'open the area' — room clicks were removed
  in v1.40.1 (the link icon does it)
- +1 unit test (120); both changelogs updated
2026-07-27 14:21:50 +03:00
Matysh 09b0ba41a5 fix v1.44.4: audit follow-up B2, B5, L4 sub-item
B2: the HTTP upload view failed OPEN when the config entry was
unavailable while the WS path failed closed — both now share one
may_write() policy helper (new auth.py) that denies non-admins when the
policy cannot be read.

B5: _finite now guards room rects, polygon vertices, view_box and
opening coordinates, not just layout positions; the declared
MAX_OPENINGS cap is finally enforced.

L4 (sub-item): every drag pipeline captures the pointer through the
tolerant helper (an inactive pointerId used to kill device/label/resize
drags); decor shapes gained a bounds clamp so they cannot be dragged far
outside the plan and persisted there.

+2 backend tests (16); both changelogs updated in this commit
2026-07-27 14:14:25 +03:00
Matysh 0467cee98a fix v1.44.3: signed content paths — plans and PDFs load again (B1 regression)
The v1.43.0 auth fix closed the hole but left the DISPLAY path
unauthenticated: HA authenticates by a Bearer header or an authSig
signed path, and an <image href> / <a href> sends neither, so plan
backgrounds and manual links returned 401. Reproduced live before the
fix (fetch 401, Image onerror).

- new WS houseplan/content/sign mints async_sign_path urls (24 h,
  bound to the connection's refresh token, only for our own endpoint)
- the card resolves display urls through _display(): signed when known,
  requests a batched signature otherwise, re-renders when it lands, and
  drops all signatures every 12 h so long-lived wall tablets stay valid
- houseplan-space-card signs its background too
- backend test asserts the unsigned url is refused and the signed one
  returns the bytes WITHOUT an Authorization header
2026-07-27 14:08:29 +03:00
Matysh c0653dfc73 docs: add docs/CHANGELOG.ru.md (Russian changelog from v1.42.0)
- 10 most recent releases translated; older entries stay English-only
- policy updated in STATUS.md and CONTRIBUTING: user-visible changes go
  into BOTH changelogs in the same commit (the user base is largely
  Russian-speaking — see the Telegram chat)
- cross-links between the two files and from both READMEs
2026-07-27 13:57:48 +03:00
Matysh 946e7543ad Merge dev: v1.43.3..v1.44.2 (feedback fixes, control-first card, review CR-1..CR-3) 2026-07-27 13:05:03 +03:00
Matysh 641c61dc19 test: the files-migrate test now sets the integration up like its neighbours
the new CR-2/CR-3 test sent WS commands without a config entry, so the
handlers were not registered and CI reported success=False
2026-07-27 13:02:10 +03:00
Matysh ae9168f6ec fix v1.44.2: external review CR-1..CR-3
CR-1: the lock invariant is restated precisely (never by an accidental
tap; the door card's labeled button is the ONE sanctioned surface),
unlocking now confirms, and smoke_lock_invariant exercises all five
actuation paths (icon tap, controls[], card entities, _cardToggle,
opening card).

CR-2: attachment migration is transactional — the server COPIES files,
the config is committed with its revision check, and only then the old
folder is removed via the new houseplan/files/cleanup. A rejected save
no longer leaves the stored urls pointing at an emptied folder.

CR-3: migrate returns an exact {source: written} mapping; only confirmed
copies are rewritten, destination name collisions get a unique name
instead of silently linking a pre-existing file, and a failed migration
raises a toast instead of being swallowed.

+1 unit test (119), +1 backend test, +1 smoke (51 total); docs
same-commit
2026-07-27 12:58:27 +03:00