mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
L2 (silent data loss): debounce gains flush()/pending(); _reloadConfigOnly flushes a pending write and defers while one is in flight; conflict path forces; failed reload now toasts instead of an empty catch; teardown flushes. G1 (split corruption): same-edge cuts carve the niche properly instead of walking the outline twice; partition invariant (parts sum to the original) rejects anything else; +1 unit test covering 5 niche shapes and both legacy cut shapes. B1 (unauthenticated content): plans and marker files move to HouseplanContentView (/api/houseplan/content/..., requires_auth); only the card bundle stays static; contentUrl() rewrites legacy URLs on read (no storage migration); repairs.py accepts both prefixes; +1 unit test. L3 (dialog zombies): all four save catch-blocks guard against a closed dialog; the card no longer blanks when a save fails after Esc. smokes: smoke_save_race, smoke_dialog_zombie; docs (TESTING/CHANGELOG/ ARCHITECTURE incl. the optimistic-UI note) same-commit
59 lines
2.2 KiB
Python
59 lines
2.2 KiB
Python
"""Repair issues for House Plan.
|
|
|
|
The check runs at entry setup AND after every config save (ws_config_set),
|
|
so a plan file that goes missing — or gets re-uploaded — is reflected in the
|
|
Repairs UI without waiting for a restart.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import issue_registry as ir
|
|
|
|
from .const import CONTENT_URL, DOMAIN, PLANS_DIR, PLANS_URL
|
|
from .store import HouseplanConfigEntry
|
|
|
|
|
|
async def async_check_plan_files(hass: HomeAssistant, entry: HouseplanConfigEntry) -> None:
|
|
"""Raise an issue for every space whose plan file is missing on disk."""
|
|
cfg_raw = await entry.runtime_data.config_store.async_load() or {}
|
|
spaces = cfg_raw.get("config", {}).get("spaces", [])
|
|
plans_dir = Path(hass.config.path(PLANS_DIR))
|
|
|
|
def _missing() -> list[tuple[str, str]]:
|
|
res = []
|
|
for sp in spaces:
|
|
url = sp.get("plan_url") or ""
|
|
# both the legacy static URL and the authenticated content URL
|
|
prefix = None
|
|
if url.startswith(PLANS_URL + "/"):
|
|
prefix = PLANS_URL + "/"
|
|
elif url.startswith(CONTENT_URL + "/plans/_/"):
|
|
prefix = CONTENT_URL + "/plans/_/"
|
|
if prefix is None:
|
|
continue # external/legacy URL — not ours to verify
|
|
fname = url[len(prefix) :].split("?", 1)[0]
|
|
if not (plans_dir / fname).is_file():
|
|
res.append((sp.get("id", "?"), fname))
|
|
return res
|
|
|
|
missing = await hass.async_add_executor_job(_missing)
|
|
broken = set()
|
|
for space_id, fname in missing:
|
|
broken.add(space_id)
|
|
ir.async_create_issue(
|
|
hass,
|
|
DOMAIN,
|
|
f"broken_plan_{space_id}",
|
|
is_fixable=False,
|
|
severity=ir.IssueSeverity.WARNING,
|
|
translation_key="broken_plan",
|
|
translation_placeholders={"space": space_id, "file": fname},
|
|
)
|
|
# clear stale issues for spaces that are fine again (or gone)
|
|
for sp in spaces:
|
|
sid = sp.get("id", "?")
|
|
if sid not in broken:
|
|
ir.async_delete_issue(hass, DOMAIN, f"broken_plan_{sid}")
|