mirror of
https://github.com/Matysh/houseplan-card
synced 2026-08-01 00:48:29 +00:00
v1.45.1: follow-up review of v1.45.0 — R3-1, R3-2
R3-1 (high): v1.45.0 made the upload safe but left deletion to the client — after a successful save the card asked the backend to remove everything but the file it had just committed. Two open editors cannot be ordered: a delayed request from one deleted the plan the other had just saved, leaving the accepted configuration pointing at nothing, the exact damage copy-on-write was introduced to prevent. houseplan/plan/cleanup is removed. config/set collects inside its own write lock from the two configurations that bracket the commit (plans.collect_plans): a file the old revision referenced and the new one does not is superseded and goes; any other unreferenced upload waits out PLAN_ORPHAN_TTL_S, because a fresh one may belong to a transaction that has not committed yet. The collector lives in a pure module so it can be reasoned about and unit-tested without the HA harness. R3-2: houseplan-space-card signed its plan url and threw the result away — getCardSize() mutated a throwaway model while render() rebuilt its own from the config, so the <image> requested the protected path and got 401 on every render. Both cards now share ContentSigner (src/signing.ts), which also gives the static card batching, expiry handling and periodic re-signing. is released in finally: one failed request no longer wedges a url for the life of the page. Tests: five backend interleaving cases from the report, six unit tests for the pure collector, smoke_space_card_bg (verified to fail against a v1.45.0 build: the raw url reaches the DOM and no retry happens). 57 smokes, 124 unit, 22 backend-pure. Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
This commit is contained in:
+16
-20
@@ -10,6 +10,8 @@ import { cardStyles } from './styles';
|
||||
import { renderSpaceStatic, spaceModels } from './space-render';
|
||||
import { getConfig, onConfigChange, cachedSnapshot, type HpConfigSnapshot } from './config-store';
|
||||
import { t, langOf, type Lang } from './i18n';
|
||||
import { ContentSigner } from './signing';
|
||||
import { referencedContentUrls } from './logic';
|
||||
import './space-editor';
|
||||
|
||||
const fireEvent = (node: EventTarget, type: string, detail?: unknown) => {
|
||||
@@ -73,11 +75,14 @@ class HouseplanSpaceCard extends LitElement {
|
||||
this._snap = null;
|
||||
this.requestUpdate();
|
||||
});
|
||||
// a dashboard on a wall tablet outlives a 24 h signature
|
||||
this._signer.start(() => this.hass, () => this._referenced());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
this._unsub?.();
|
||||
this._unsub = undefined;
|
||||
this._signer.dispose();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -109,8 +114,6 @@ class HouseplanSpaceCard extends LitElement {
|
||||
|
||||
public getCardSize(): number {
|
||||
const models = spaceModels(this._snap?.config || null);
|
||||
// B1 follow-up: the plan <image> needs a signed url in a browser session
|
||||
for (const m of models) if (m.bg?.href) this._signBg(m.bg);
|
||||
const sp = models.find((s) => s.id === this._config?.space);
|
||||
if (sp) {
|
||||
const ratio = sp.vb[3] / sp.vb[2]; // h/w
|
||||
@@ -123,25 +126,16 @@ class HouseplanSpaceCard extends LitElement {
|
||||
return html`<ha-card><div class="hp-static-error">${msg}</div></ha-card>`;
|
||||
}
|
||||
|
||||
private _signedBg: Record<string, string> = {};
|
||||
private _signBgPending = new Set<string>();
|
||||
/**
|
||||
* Same signer as the main card (review R3-2). The previous copy here signed
|
||||
* the url and then threw it away: `getCardSize()` mutated a throwaway model
|
||||
* while `render()` rebuilt its own from the config, so the <image> kept
|
||||
* asking for the raw protected path and got a 401 on every render.
|
||||
*/
|
||||
private _signer = new ContentSigner(() => this.requestUpdate());
|
||||
|
||||
/** Ask the backend for a signed content url and swap it in when it arrives. */
|
||||
private _signBg(bg: { href: string }): void {
|
||||
const raw = bg.href.split('?authSig=')[0];
|
||||
if (!raw.startsWith('/api/houseplan/content/')) return;
|
||||
if (this._signedBg[raw]) { bg.href = this._signedBg[raw]; return; }
|
||||
if (this._signBgPending.has(raw) || !this.hass?.callWS) return;
|
||||
this._signBgPending.add(raw);
|
||||
this.hass
|
||||
.callWS({ type: 'houseplan/content/sign', paths: [raw] })
|
||||
.then((r: any) => {
|
||||
const url = r?.urls?.[raw];
|
||||
if (!url) return;
|
||||
this._signedBg = { ...this._signedBg, [raw]: url };
|
||||
this.requestUpdate();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
private _referenced(): Set<string> {
|
||||
return referencedContentUrls(this._snap?.config);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
@@ -159,6 +153,8 @@ class HouseplanSpaceCard extends LitElement {
|
||||
spaceId,
|
||||
iconSize: this._config.icon_size,
|
||||
lang: this._lang,
|
||||
// resolved at render time: a url baked in earlier would be the unsigned one
|
||||
displayUrl: (raw) => this._signer.display(this.hass, raw),
|
||||
});
|
||||
if (!stage) {
|
||||
return this._errorCard(t(this._lang, 'space_card.not_found', { id: spaceId }));
|
||||
|
||||
Reference in New Issue
Block a user