mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
R2-1 (high): plan replacement committed filesystem state before the config CAS.
The upload wrote the final name and unlinked the other extension, so a rejected
config write left the live plan already replaced — or the stored config
pointing at a deleted file. Uploads now go to <space>.<token>.<ext> and delete
nothing; houseplan/plan/cleanup runs only after the config write is accepted.
The '.' separator is load-bearing: a space id cannot contain one, so cleaning
'f1' can never reach the files of 'f1-attic'.
R2-2: the backend signs at most MAX_SIGN_PATHS (200) per request and ignores
the rest silently, while the card sent its whole cache in one call and trusted
any cached entry forever — past 200 attachments the later ones stopped being
refreshed and expired for good. Requests are chunked to the shared constant,
entries carry their issue time (aging urls keep rendering while a replacement
is fetched, expired ones are dropped), and the cache is pruned to urls the live
config still references.
R2-3: areaClimate() rescanned the whole registry per room and per measurement.
areaClimateMap() classifies once and returns Map<area,{temp,hum}>, memoized on
hass identity so fresh states are always observed. Smoke measurement: 133
registry scans per update with 44 rooms before, 2 after, flat in room count.
Also: smoke_ux_fixes wrote its screenshot to a hard-coded /tmp path and could
not run on Windows.
Tests: smoke_plan_upload_reject, smoke_sign_cap, smoke_climate_once (all fail
on v1.44.8), three backend tests for versioned plan names and cleanup scoping,
unit tests for chunk/referencedContentUrls and areaClimateMap.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
77 lines
3.6 KiB
JavaScript
77 lines
3.6 KiB
JavaScript
// Ревью R2-3: климат комнат считался отдельным обходом реестра на каждую
|
||
// комнату и каждую величину — 60 комнат × 2000 сущностей съедали кадр на
|
||
// перечитывании метаданных, которые не менялись. Карта строится один раз на
|
||
// снимок hass; при этом новые состояния датчиков обязаны попадать в неё сразу.
|
||
import { launch, checkAll, finish } from './serve.mjs';
|
||
const { page, browser } = await launch();
|
||
const res = await page.evaluate(async () => {
|
||
const out = {};
|
||
const c = window.__card;
|
||
const sr = () => c.shadowRoot || c.renderRoot;
|
||
|
||
// считаем обходы реестра через ownKeys — именно его дёргает Object.entries
|
||
let scans = 0;
|
||
const wrap = (h) => {
|
||
const ents = h.entities;
|
||
const traced = new Proxy(ents, { ownKeys(t) { scans++; return Reflect.ownKeys(t); } });
|
||
return { ...h, entities: traced };
|
||
};
|
||
const fresh = () => wrap(window.__mkHass());
|
||
|
||
// включаем и заливку по температуре, и подписи — два потребителя климата
|
||
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== 'f1' ? s : {
|
||
...s, settings: { ...(s.settings || {}), show_names: true, fill_mode: 'temp', label_temp: true, label_hum: true },
|
||
})};
|
||
c._cfgEpoch++;
|
||
c.hass = fresh(); await c.updateComplete;
|
||
|
||
scans = 0;
|
||
c.hass = fresh(); await c.updateComplete;
|
||
const fewRooms = scans;
|
||
|
||
// повторные рендеры на том же снимке hass реестр не трогают
|
||
scans = 0;
|
||
c.requestUpdate(); await c.updateComplete;
|
||
c.requestUpdate(); await c.updateComplete;
|
||
out.scansOnRerender = scans;
|
||
|
||
// главный инвариант: обходов НЕ становится больше от числа комнат
|
||
const f1 = c._serverCfg.spaces.find((s) => s.id === 'f1');
|
||
const extra = [];
|
||
for (let i = 0; i < 40; i++) {
|
||
extra.push({ id: 'gen' + i, name: 'R' + i, area: 'living_room',
|
||
poly: [[0.01, 0.01], [0.02, 0.01], [0.02, 0.02], [0.01, 0.02]] });
|
||
}
|
||
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) =>
|
||
s.id !== 'f1' ? s : { ...s, rooms: [...s.rooms, ...extra] }) };
|
||
c._cfgEpoch++;
|
||
c.hass = fresh(); await c.updateComplete;
|
||
scans = 0;
|
||
c.hass = fresh(); await c.updateComplete;
|
||
out.roomCount = c._spaceModel('f1').rooms.length;
|
||
out.scansSameWith44Rooms = scans === fewRooms;
|
||
out.scansPerUpdate = scans;
|
||
|
||
// при этом новое состояние датчика обязано быть видно, а не взято из кэша
|
||
out.tempBefore = c._climate().get('living_room')?.temp;
|
||
const h = fresh();
|
||
h.states = { ...h.states, 'sensor.living_temp': { ...h.states['sensor.living_temp'], state: '33.3' } };
|
||
c.hass = h; await c.updateComplete;
|
||
out.tempAfter = c._climate().get('living_room')?.temp;
|
||
out.climateIsMap = c._climate() instanceof Map;
|
||
return out;
|
||
});
|
||
// зафиксировано прогоном на v1.45.0 и сверено с кодом.
|
||
// scansPerUpdate = 2: один обход у areaClimateMap, один у buildDevices. Важно
|
||
// не само число, а что оно не растёт вместе с числом комнат.
|
||
checkAll(res, {
|
||
scansOnRerender: 0,
|
||
roomCount: 44,
|
||
scansSameWith44Rooms: true,
|
||
scansPerUpdate: 2,
|
||
tempBefore: 22.4,
|
||
tempAfter: 33.3,
|
||
climateIsMap: true,
|
||
});
|
||
await finish(browser);
|