mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
v1.46.2: re-check of v1.46.1 — HP-1461-01, -02
HP-1461-01: collection was tied to config/set, which is the right scope for what a commit supersedes but leaves a file nobody references with no future write to notice it — cancel a dialog after the upload finished, drop the connection just after, or call the upload API directly. The daily sweep added in v1.46.1 only removed streaming temporaries, so the documented 'a cancelled attachment is collected an hour later' did not hold on an instance nobody edits. The scheduled pass now loads the stored config under the same write_lock a commit uses and runs collect_attachments/collect_plans with it as BOTH sides: nothing counts as superseded, referenced files are preserved, aged unreferenced ones go. Doing it under the lock keeps it from deciding on a snapshot a commit is about to replace. HP-1461-02: _reloadLayoutOnly captured the dirty set AFTER flushing the pending write, and the flush empties it first — so during a real drag (where a write is already scheduled) the snapshot was empty and the server's older position was merged over the user's move. The snapshot is taken before the flush, by value, and a _sentPos map now holds positions that are sent but unacknowledged, which closes the same window for a write that was already in flight. Tests: the upload test now cancels the request task for real (the previous one claimed to and only walked error paths); smoke_layout_sync schedules a genuine debounced write and delays it — verified failing on a v1.46.1 build with exactly the reported symptom; a new backend test reloads the entry and asserts the scheduled sweep takes an aged cancelled attachment and an orphan plan while keeping everything the config still references. Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
This commit is contained in:
+20
-4
@@ -35,7 +35,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.46.1';
|
||||
const CARD_VERSION = '1.46.2';
|
||||
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';
|
||||
@@ -873,13 +873,23 @@ class HouseplanCard extends LitElement {
|
||||
*/
|
||||
private async _reloadLayoutOnly(): Promise<void> {
|
||||
if (!this._serverStorage || !this.hass?.callWS) return;
|
||||
// Snapshot BEFORE flushing. `flush()` runs the debounced writer
|
||||
// synchronously, and the first thing that does is empty `_dirtyPos` — so
|
||||
// reading the dirty set afterwards found nothing to protect and the server's
|
||||
// older position was painted over the drag the user had just made
|
||||
// (HP-1461-02). Positions are captured by value for the same reason.
|
||||
const mine = new Map<string, any>();
|
||||
for (const id of this._dirtyPos) if (this._layout[id]) mine.set(id, this._layout[id]);
|
||||
if (this._persistLayout.pending()) this._persistLayout.flush();
|
||||
const mine = new Set(this._dirtyPos);
|
||||
// …and again after the flush: what was dirty is now in flight, and a write
|
||||
// sent before this reload was even scheduled is in there too. Until the
|
||||
// server acknowledges a position, this card is the authority on it.
|
||||
for (const [id, pos] of this._sentPos) mine.set(id, pos);
|
||||
try {
|
||||
const resp = await this.hass.callWS({ type: 'houseplan/layout/get' });
|
||||
const remote = resp?.layout || {};
|
||||
const merged: Record<string, any> = { ...remote };
|
||||
for (const id of mine) if (this._layout[id]) merged[id] = this._layout[id];
|
||||
for (const [id, pos] of mine) merged[id] = pos;
|
||||
this._layout = merged;
|
||||
this._layoutRev = resp?.rev ?? this._layoutRev;
|
||||
this._cacheSnapshot();
|
||||
@@ -890,6 +900,8 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
|
||||
private _dirtyPos = new Set<string>();
|
||||
/** Positions sent to the server and not acknowledged yet (HP-1461-02). */
|
||||
private _sentPos = new Map<string, { s?: string; x: number; y: number }>();
|
||||
|
||||
private _persistLayout = debounce(() => {
|
||||
if (this._serverStorage) {
|
||||
@@ -899,10 +911,14 @@ class HouseplanCard extends LitElement {
|
||||
for (const id of ids) {
|
||||
const pos = this._layout[id];
|
||||
if (!pos) continue;
|
||||
// in flight until the server answers: a layout reload triggered in the
|
||||
// meantime must keep this position, not the one the server still has
|
||||
this._sentPos.set(id, pos);
|
||||
this.hass
|
||||
.callWS({ type: 'houseplan/layout/update', device_id: id, pos })
|
||||
.then((r: any) => this._noteLayoutRev(r))
|
||||
.catch((e: any) => this._showToast(this._t('toast.pos_save_failed', { err: this._errText(e) })));
|
||||
.catch((e: any) => this._showToast(this._t('toast.pos_save_failed', { err: this._errText(e) })))
|
||||
.finally(() => { if (this._sentPos.get(id) === pos) this._sentPos.delete(id); });
|
||||
}
|
||||
this._cacheSnapshot();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user