mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +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.
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
/**
|
|
* Module-level houseplan config cache shared by every embedded card on a board,
|
|
* so N cards do NOT issue N identical `houseplan/config/get` requests. The cache
|
|
* is invalidated when the integration emits `houseplan_config_updated`
|
|
* (fired on config/set); subscribers are then notified to reload.
|
|
*
|
|
* The full `houseplan-card` already persists a `{config, rev, layout}` snapshot in
|
|
* localStorage (`houseplan_card_cfg_v1`) for its own instant start — we seed from it
|
|
* so embedded cards paint immediately, then refresh from the server in the background.
|
|
*/
|
|
const LS_CFG = 'houseplan_card_cfg_v1';
|
|
|
|
export interface HpConfigSnapshot {
|
|
config: any | null;
|
|
rev: number;
|
|
layout: Record<string, any>;
|
|
}
|
|
|
|
let cache: HpConfigSnapshot | null = null;
|
|
let inflight: Promise<HpConfigSnapshot> | null = null;
|
|
let subscribed = false;
|
|
const listeners = new Set<() => void>();
|
|
|
|
/** Instant, synchronous best-effort snapshot from the full card's localStorage cache. */
|
|
export function cachedSnapshot(): HpConfigSnapshot | null {
|
|
if (cache) return cache;
|
|
try {
|
|
const c = JSON.parse(localStorage.getItem(LS_CFG) || 'null');
|
|
if (c && c.config && Array.isArray(c.config.spaces)) {
|
|
return { config: c.config, rev: c.rev || 0, layout: c.layout || {} };
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function fetchFresh(hass: any): Promise<HpConfigSnapshot> {
|
|
const [cfgResp, layResp] = await Promise.all([
|
|
hass.callWS({ type: 'houseplan/config/get' }),
|
|
hass.callWS({ type: 'houseplan/layout/get' }),
|
|
]);
|
|
cache = {
|
|
config: cfgResp?.config ?? null,
|
|
rev: cfgResp?.rev ?? 0,
|
|
layout: layResp?.layout ?? {},
|
|
};
|
|
if (!subscribed && hass.connection?.subscribeEvents) {
|
|
subscribed = true;
|
|
const invalidate = () => {
|
|
cache = null; // invalidate; listeners reload
|
|
listeners.forEach((l) => l());
|
|
};
|
|
try {
|
|
await hass.connection.subscribeEvents(invalidate, 'houseplan_config_updated');
|
|
// Layout is separate state: dragging an icon on the full card writes only
|
|
// the layout, so a static card on the same dashboard kept showing the old
|
|
// position until the config changed or the page was reloaded — possibly
|
|
// forever on a wall tablet (HP-1454-08).
|
|
await hass.connection.subscribeEvents(invalidate, 'houseplan_layout_updated');
|
|
} catch {
|
|
subscribed = false;
|
|
}
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
/** Get the shared config snapshot (cached, deduped across cards). */
|
|
export function getConfig(hass: any): Promise<HpConfigSnapshot> {
|
|
if (cache) return Promise.resolve(cache);
|
|
if (inflight) return inflight;
|
|
inflight = fetchFresh(hass).finally(() => {
|
|
inflight = null;
|
|
});
|
|
return inflight;
|
|
}
|
|
|
|
/** Subscribe to config-changed notifications; returns an unsubscribe function. */
|
|
export function onConfigChange(cb: () => void): () => void {
|
|
listeners.add(cb);
|
|
return () => listeners.delete(cb);
|
|
}
|