mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
Found on the owner's install: the image lands in /config/houseplan/plans, the space keeps plan_url=null, the plan never shows and re-saving does not help. _saveSpaceDialog held a reference to the space object across the await that uploads the file. _reloadConfigOnly() — which runs on every houseplan_config_updated event — REPLACES _serverCfg, so that reference became an orphan: plan_url, aspect, title and every display setting were written into a detached object while the save shipped the untouched config. In 'create' mode the whole new space was lost the same way. - upload first, then touch the config; no reference is held across an await. - _saveConfigNow() sets _cfgWriting like the debounced writer, so a revision arriving mid-save defers its reload instead of replacing the config (audit L2 extended to this path). - demo/smoke_plan_upload_race.mjs: on v1.44.7 the sent config still carries the OLD plan_url and the created space is missing; passes here. The demo's config/get now returns a fresh object, as a real server does — returning the same reference is what hid this class of bug from the smoke layer. - DEVELOPMENT.md: the deploy target is custom_components/houseplan/frontend/, and deploy verification must go over HTTP. A copy placed next to __init__.py is served by nobody — that cost two deployments today. - docs: CHANGELOG.md + CHANGELOG.ru.md + TESTING.md + STATUS.md.
71 lines
3.5 KiB
JavaScript
71 lines
3.5 KiB
JavaScript
// Загрузка подложки: ссылка обязана долететь до конфига.
|
|
// Баг 2026-07-27 (найден на боевой установке): _saveSpaceDialog держал ссылку
|
|
// на объект пространства через await загрузки файла. Любое событие
|
|
// houseplan_config_updated в этот момент вызывает _reloadConfigOnly(), которое
|
|
// ЗАМЕНЯЕТ _serverCfg — и plan_url/aspect/settings уезжали в осиротевший
|
|
// объект, а на сервер уходил нетронутый конфиг. Симптом: файл на диске есть,
|
|
// подложки нет, пересохранение не помогает.
|
|
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;
|
|
let reloadDuringUpload = 0;
|
|
|
|
c.hass = { ...c.hass, callWS: async (m) => {
|
|
if (m.type === 'houseplan/plan/set') {
|
|
// пока файл «загружается», прилетает чужая ревизия конфига
|
|
reloadDuringUpload++;
|
|
await c._reloadConfigOnly(true);
|
|
return { ok: true, url: '/api/houseplan/content/plans/_/' + m.space_id + '.png?v=42' };
|
|
}
|
|
if (m.type === 'houseplan/config/set') { c.__sent = m.config; return { ok: true, rev: 99 }; }
|
|
if (m.type === 'houseplan/config/get') {
|
|
// сервер отдаёт СВЕЖИЙ объект, а не тот же самый — как в реальном HA
|
|
const r = await base(m);
|
|
return { ...r, config: JSON.parse(JSON.stringify(r.config)) };
|
|
}
|
|
return base(m);
|
|
} };
|
|
|
|
// редактирование существующего пространства: подложка + новый заголовок
|
|
c._openSpaceDialog('edit', 'f1'); await c.updateComplete;
|
|
c._spaceDialog = { ...c._spaceDialog, title: 'Ground', source: 'file',
|
|
planFile: { ext: 'png', b64: 'AAAA', aspect: 1.6 } };
|
|
await c._saveSpaceDialog(); await c.updateComplete;
|
|
|
|
out.reloadHappened = reloadDuringUpload === 1;
|
|
const sentF1 = (c.__sent?.spaces || []).find((s) => s.id === 'f1');
|
|
const liveF1 = (c._serverCfg?.spaces || []).find((s) => s.id === 'f1');
|
|
out.sentPlanUrl = sentF1?.plan_url;
|
|
out.sentAspect = sentF1?.aspect;
|
|
out.sentTitle = sentF1?.title;
|
|
out.livePlanUrl = liveF1?.plan_url;
|
|
out.dialogClosed = c._spaceDialog === null;
|
|
|
|
// создание пространства при том же сбое: оно должно доехать целиком
|
|
c._openSpaceDialog('create'); await c.updateComplete;
|
|
c._spaceDialog = { ...c._spaceDialog, title: 'Attic', source: 'file',
|
|
planFile: { ext: 'png', b64: 'BBBB', aspect: 0.8 } };
|
|
await c._saveSpaceDialog(); await c.updateComplete;
|
|
const attic = (c.__sent?.spaces || []).find((s) => s.title === 'Attic');
|
|
out.atticSaved = !!attic;
|
|
out.atticHasPlan = !!attic && typeof attic.plan_url === 'string' && attic.plan_url.includes('/content/plans/');
|
|
out.atticAspect = attic?.aspect;
|
|
return out;
|
|
});
|
|
// зафиксировано прогоном на v1.44.8 и сверено с кодом
|
|
checkAll(res, {
|
|
reloadHappened: true,
|
|
sentPlanUrl: '/api/houseplan/content/plans/_/f1.png?v=42',
|
|
sentAspect: 1.6,
|
|
sentTitle: 'Ground',
|
|
livePlanUrl: '/api/houseplan/content/plans/_/f1.png?v=42',
|
|
dialogClosed: true,
|
|
atticSaved: true,
|
|
atticHasPlan: true,
|
|
atticAspect: 0.8,
|
|
});
|
|
await finish(browser);
|