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
This commit is contained in:
Matysh
2026-07-27 14:14:25 +03:00
parent 0467cee98a
commit 09b0ba41a5
15 changed files with 211 additions and 54 deletions
+35 -9
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.44.2';
const CARD_VERSION = '1.44.4';
const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
@@ -90,6 +90,22 @@ const debounce = <T extends (...a: any[]) => void>(fn: T, ms: number): Debounced
return wrapped;
};
/**
* Capture the pointer for a drag, tolerating an inactive pointerId.
*
* `setPointerCapture` throws for synthetic events and for pointers some
* browsers consider gone; that killed a drag outright. The opening pipeline
* was hardened for this, the device/label/resize ones were not (audit
* follow-up L4 sub-item) — now they all go through here.
*/
const capturePointer = (ev: PointerEvent): void => {
try {
(ev.target as Element | null)?.setPointerCapture?.(ev.pointerId);
} catch {
/* an inactive pointerId must never kill the drag */
}
};
class HouseplanCard extends LitElement {
public hass?: any;
private _config?: CardConfig;
@@ -1358,7 +1374,7 @@ class HouseplanCard extends LitElement {
ev.preventDefault();
const p = this._pos(d);
this._drag = { id: d.id, sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false };
(ev.target as HTMLElement).setPointerCapture(ev.pointerId);
capturePointer(ev);
this._tip = null;
}
@@ -1701,7 +1717,7 @@ class HouseplanCard extends LitElement {
ev.preventDefault();
const p = this._snap(this._svgPoint(ev));
this._decorDraft = { kind: t, a: p, b: p, pid: ev.pointerId };
(ev.target as HTMLElement).setPointerCapture?.(ev.pointerId);
capturePointer(ev);
return true;
}
if (t === 'text') {
@@ -1761,15 +1777,25 @@ class HouseplanCard extends LitElement {
id: shape.id, start: this._svgPoint(ev), orig: JSON.parse(JSON.stringify(shape)),
pid: ev.pointerId, moved: false,
};
(ev.target as HTMLElement).setPointerCapture?.(ev.pointerId);
capturePointer(ev);
}
private _decorMoveUpdate(ev: PointerEvent): void {
const m = this._decorMove!;
const p = this._svgPoint(ev);
const g = this._gridPitch;
const dx = snapToGrid(p[0] - m.start[0], g) / NORM_W;
const dy = snapToGrid(p[1] - m.start[1], g) / this._decorH;
let dx = snapToGrid(p[0] - m.start[0], g) / NORM_W;
let dy = snapToGrid(p[1] - m.start[1], g) / this._decorH;
// audit follow-up L4: decor had neither a threshold nor a bounds clamp, so
// a shape could be dragged far outside the viewBox and persisted there.
const o = m.orig;
const curX = o.kind === 'line' ? Math.min(o.x1, o.x2) : o.x;
const curY = o.kind === 'line' ? Math.min(o.y1, o.y2) : o.y;
const w = o.kind === 'line' ? Math.abs(o.x2 - o.x1) : (o.w || 0);
const h = o.kind === 'line' ? Math.abs(o.y2 - o.y1) : (o.h || 0);
const lim = 0.25; // a quarter of the plan may hang outside, no more
dx = Math.max(-curX - lim, Math.min(1 + lim - curX - w, dx));
dy = Math.max(-curY - lim, Math.min(1 + lim - curY - h, dy));
if (dx || dy) m.moved = true;
const sp = this._curSpaceCfg;
sp.decor = this._decorList.map((x) => {
@@ -2080,7 +2106,7 @@ class HouseplanCard extends LitElement {
ev.preventDefault();
ev.stopPropagation();
try {
(ev.target as Element).setPointerCapture?.(ev.pointerId);
capturePointer(ev);
} catch {
/* an inactive pointerId (synthetic events, some browsers) must not kill the drag */
}
@@ -3929,7 +3955,7 @@ class HouseplanCard extends LitElement {
ev.stopPropagation();
const p = this._labelPos(r, spaceId);
this._drag = { id: 'rl_' + (r.id || ''), sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false };
(ev.target as HTMLElement).setPointerCapture(ev.pointerId);
capturePointer(ev);
this._tip = null;
}
@@ -3975,7 +4001,7 @@ class HouseplanCard extends LitElement {
const cy = b.top + b.height / 2;
const d0 = Math.max(8, Math.hypot(ev.clientX - cx, ev.clientY - cy));
this._rlResize = { id: 'rl_' + (r.id || ''), space: spaceId, k0: this._labelScale(r), cx, cy, d0 };
(ev.target as HTMLElement).setPointerCapture(ev.pointerId);
capturePointer(ev);
}
private _rlResizeMove(ev: PointerEvent): void {