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.
This commit is contained in:
Matysh
2026-07-28 22:44:09 +03:00
parent e1e730560d
commit f5e6c0318d
27 changed files with 638 additions and 140 deletions
+46
View File
@@ -1161,3 +1161,49 @@ async def test_stored_plans_can_be_listed_and_deleted_on_request(
)
bad = await client.receive_json()
assert not bad["success"] and bad["error"]["code"] == "invalid_name"
async def test_config_set_refuses_a_plan_that_no_longer_exists(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""HP-1470-02: a stored internal plan url must name a file that exists.
The card can pick a plan and delete it from the same dialog, and two clients
can do the same in either order. The lock serialises them; it says nothing
about whether the file survived, so the check has to be here.
"""
await _setup(hass)
client = await hass_ws_client(hass)
url, name = await _upload(client, "x1", b"PLAN", ext="png")
await client.send_json_auto_id({"type": "houseplan/plans/delete", "name": name})
assert (await client.receive_json())["result"]["removed"] is True
resp = await _save(client, await _cfg([{"id": "x1", "plan_url": url}]), 0)
assert not resp["success"] and resp["error"]["code"] == "missing_plan"
# an external or legacy url is the user's business, not ours to verify
assert (await _save(client, await _cfg([{"id": "x1", "plan_url": "/local/mine.png"}]), 0))["success"]
async def test_uploads_are_bounded_by_a_store_quota(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, monkeypatch
) -> None:
"""HP-1470-01: nothing is deleted for being old, so growth stops at the door."""
from custom_components.houseplan import websocket_api as wsapi
await _setup(hass)
client = await hass_ws_client(hass)
monkeypatch.setattr(wsapi, "MAX_PLANS_FILES", 2)
await _upload(client, "q1", b"one")
await _upload(client, "q1", b"two")
import base64
await client.send_json_auto_id({
"type": "houseplan/plan/set", "space_id": "q1", "ext": "png",
"data": base64.b64encode(b"three").decode(),
})
resp = await client.receive_json()
assert not resp["success"] and resp["error"]["code"] == "too_many_files"
+43
View File
@@ -780,3 +780,46 @@ def test_migration_runs_once_and_only_when_needed():
snapshot = repr(cfg)
assert gm.migrate_config(cfg, {}) is False, "already square: nothing to do"
assert repr(cfg) == snapshot
# ---------- store-wide limits (HP-1470-01) ----------
def test_check_quota_counts_the_whole_store_not_one_request(tmp_path):
"""Per-request caps say nothing about how many requests there are."""
d = tmp_path / "plans"
d.mkdir()
for i in range(3):
(d / f"p{i}.png").write_bytes(b"x" * 1000)
plans.check_quota(d, 1000, max_bytes=10_000, max_files=10) # fits
with pytest.raises(plans.QuotaError) as e:
plans.check_quota(d, 8000, max_bytes=10_000, max_files=10)
assert e.value.reason == "quota_exceeded" and "MB" in e.value.detail
with pytest.raises(plans.QuotaError) as e:
plans.check_quota(d, 1, max_bytes=10_000, max_files=3)
assert e.value.reason == "too_many_files"
def test_dir_usage_walks_subfolders_and_ignores_the_unreadable(tmp_path):
d = tmp_path / "files"
(d / "m1").mkdir(parents=True)
(d / "m1" / "a.pdf").write_bytes(b"x" * 10)
(d / "b.pdf").write_bytes(b"x" * 5)
assert plans.dir_usage(d) == (15, 2)
assert plans.dir_usage(tmp_path / "nope") == (0, 0)
def test_check_quota_refuses_when_the_disk_is_nearly_full(tmp_path, monkeypatch):
import shutil
d = tmp_path / "plans"
d.mkdir()
monkeypatch.setattr(
shutil, "disk_usage", lambda _p: type("U", (), {"free": const.MIN_FREE_BYTES // 2})()
)
with pytest.raises(plans.QuotaError) as e:
plans.check_quota(d, 1, max_bytes=10 ** 12, max_files=10 ** 6)
assert e.value.reason == "low_disk_space"