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.5 KiB
JavaScript
77 lines
3.5 KiB
JavaScript
// Ревью R2-2: бэкенд подписывает не более MAX_SIGN_PATHS путей за вызов и
|
|
// молча отбрасывает остальные. Карточка обязана бить запрос на батчи, помнить
|
|
// возраст подписи и чистить кэш от ссылок, которых в конфиге больше нет —
|
|
// иначе на настенном планшете «лишние» записи протухают навсегда.
|
|
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 base = c.hass.callWS;
|
|
const batchSizes = [];
|
|
let round = 0;
|
|
|
|
c.hass = { ...c.hass, callWS: async (m) => {
|
|
if (m.type === 'houseplan/content/sign') {
|
|
batchSizes.push(m.paths.length);
|
|
const urls = {};
|
|
// как настоящий бэкенд: не больше 200 за раз, про остальные — молчание
|
|
for (const p of m.paths.slice(0, 200)) urls[p] = p.split('?')[0] + '?authSig=R' + round;
|
|
return { urls };
|
|
}
|
|
return base(m);
|
|
} };
|
|
|
|
// 201 вложение, разложенное по маркерам: столько же подписанных ссылок
|
|
const pdfs = [];
|
|
for (let i = 0; i < 201; i++) pdfs.push({ name: 'm' + i, url: '/api/houseplan/content/files/m/doc' + i + '.pdf' });
|
|
c._serverCfg = { ...c._serverCfg, markers: [{ id: 'mk1', pdfs }] };
|
|
c._cfgEpoch++;
|
|
|
|
round = 1;
|
|
for (const p of pdfs) c._display(p.url);
|
|
await new Promise((r) => setTimeout(r, 120));
|
|
out.firstBatches = [...batchSizes];
|
|
out.signedAfterFirst = Object.keys(c._signed).length;
|
|
|
|
// переподписывание: все 201, снова батчами, ни одна запись не остаётся старой
|
|
batchSizes.length = 0;
|
|
round = 2;
|
|
c._resign();
|
|
await new Promise((r) => setTimeout(r, 120));
|
|
out.resignBatches = [...batchSizes];
|
|
const vals = Object.values(c._signed).map((v) => v.url);
|
|
out.allRefreshed = vals.length === 201 && vals.every((u) => u.endsWith('authSig=R2'));
|
|
|
|
// ссылка, исчезнувшая из конфига, выбывает из кэша и не занимает слот
|
|
c._serverCfg = { ...c._serverCfg, markers: [{ id: 'mk1', pdfs: pdfs.slice(0, 5) }] };
|
|
c._cfgEpoch++;
|
|
batchSizes.length = 0;
|
|
round = 3;
|
|
c._resign();
|
|
await new Promise((r) => setTimeout(r, 120));
|
|
out.prunedTo = Object.keys(c._signed).length;
|
|
out.pruneBatches = [...batchSizes];
|
|
|
|
// протухшая подпись не отдаётся: она вернула бы 401 и «попытку входа»
|
|
const one = pdfs[0].url;
|
|
c._signed = { ...c._signed, [one]: { url: one + '?authSig=OLD', at: Date.now() - 25 * 3600 * 1000 } };
|
|
out.expiredNotServed = c._display(one) === '';
|
|
// а стареющая, но ещё живая — отдаётся, пока едет замена
|
|
c._signed = { ...c._signed, [one]: { url: one + '?authSig=AGING', at: Date.now() - 20 * 3600 * 1000 } };
|
|
out.agingStillServed = c._display(one) === one + '?authSig=AGING';
|
|
return out;
|
|
});
|
|
// зафиксировано прогоном на v1.45.0 и сверено с кодом
|
|
checkAll(res, {
|
|
firstBatches: [200, 1],
|
|
signedAfterFirst: 201,
|
|
resignBatches: [200, 1],
|
|
allRefreshed: true,
|
|
prunedTo: 5,
|
|
pruneBatches: [5],
|
|
expiredNotServed: true,
|
|
agingStillServed: true,
|
|
});
|
|
await finish(browser);
|