mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
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.
68 lines
2.6 KiB
Python
68 lines
2.6 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. Iterating the CURRENT spaces could only ever clear
|
|
# issues for spaces that still exist, so deleting or renaming a space with a
|
|
# missing plan left its warning in Repairs forever, with nothing left to fix
|
|
# it (HP-1454-09). Enumerate what we actually published instead.
|
|
registry = ir.async_get(hass)
|
|
stale = [
|
|
issue_id
|
|
for (domain, issue_id) in list(registry.issues)
|
|
if domain == DOMAIN
|
|
and issue_id.startswith("broken_plan_")
|
|
and issue_id[len("broken_plan_") :] not in broken
|
|
]
|
|
for issue_id in stale:
|
|
ir.async_delete_issue(hass, DOMAIN, issue_id)
|