mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
v1.45.0: external review of v1.44.8 — R2-1, R2-2, R2-3
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.
This commit is contained in:
@@ -1036,6 +1036,48 @@ export function outlineWithout(poly: number[][], cuts: number[][], eps = 1e-6):
|
||||
* authenticated content endpoint (audit B1). Applied on READ, so stored
|
||||
* configs keep working without a migration.
|
||||
*/
|
||||
/**
|
||||
* How many paths one `houseplan/content/sign` call may carry. The backend caps
|
||||
* the request at the same number and silently ignores the rest, so a client
|
||||
* that sends more gets a partial answer with no way to tell which paths were
|
||||
* dropped — on a wall tablet those entries then expire for good (review R2-2).
|
||||
* Keep in sync with MAX_SIGN_PATHS in custom_components/houseplan/const.py.
|
||||
*/
|
||||
export const MAX_SIGN_PATHS = 200;
|
||||
|
||||
/** A signature is valid for 24 h; refresh once two thirds of it is gone. */
|
||||
export const SIGN_TTL_MS = 24 * 3600 * 1000;
|
||||
export const SIGN_REFRESH_MS = 16 * 3600 * 1000;
|
||||
|
||||
/** Split a list into chunks of at most `size` (used for signing batches). */
|
||||
export function chunk<T>(items: T[], size: number): T[][] {
|
||||
const n = Math.max(1, Math.floor(size));
|
||||
const out: T[][] = [];
|
||||
for (let i = 0; i < items.length; i += n) out.push(items.slice(i, i + n));
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every content url the given config still refers to, normalised through
|
||||
* `contentUrl`. The signature cache is pruned to this set: without it the cache
|
||||
* only grows — replaced plans and deleted attachments keep their entries, and
|
||||
* the total can cross the per-request cap even when the live config is small.
|
||||
*/
|
||||
export function referencedContentUrls(cfg: any): Set<string> {
|
||||
const out = new Set<string>();
|
||||
const add = (u: unknown) => {
|
||||
if (typeof u !== 'string' || !u) return;
|
||||
const c = contentUrl(u);
|
||||
if (c.startsWith('/api/houseplan/content/')) out.add(c);
|
||||
};
|
||||
for (const sp of cfg?.spaces || []) {
|
||||
add(sp?.plan_url);
|
||||
for (const m of sp?.markers || []) for (const p of m?.pdfs || []) add(p?.url);
|
||||
}
|
||||
for (const m of cfg?.markers || []) for (const p of m?.pdfs || []) add(p?.url);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function contentUrl(url: string | null | undefined): string {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('/houseplan_files/plans/')) {
|
||||
|
||||
Reference in New Issue
Block a user