mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
План уходил base64 в WebSocket-кадре: файл больше ~3 МиБ давал кадр больше 4 МиБ, HA закрывал сокет до обработчика, и обещанные 8 МБ были недостижимы. - бэкенд: HouseplanPlanUploadView (POST /api/houseplan/plans/upload), потоковый предел MAX_PLAN_BYTES (read_bounded), общий writer store_plan_upload для view и ws_plan_set (контракт WS без изменений); - карточка: stagePlanFile/uploadPlanFile/renderPlanBackdropGuard в backdrop-pick.ts для обоих рантаймов; PlanFilePayload хранит Blob вместо b64; SVG больше предела — тост при выборе, растр — диалог #39 только с уменьшенной копией, копия больше предела не попадает в staging, 413 называет предел; - i18n toast.plan_too_large, backdrop.over_limit_body (en/ru/de/fr); USER-GUIDE ru/en, CHANGELOG ru/en, docs/testing-notes (#617); - тесты: test/plan-upload-limit.test.mjs, tests_backend/test_plan_upload.py, test_ha_upload.py (#617), smoke_plan_upload_limit.mjs; три смока переведены с b64/plan/set на blob/fetchWithAuth; мутанты plan-upload-*; - база монолита: hostRefs +3 (общий хелпер плана вместо двух копий в рантаймах, новый тост уменьшенной копии). Issue: #617 User-Visible: yes
79 lines
4.1 KiB
JavaScript
79 lines
4.1 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;
|
|
let wsUploads = 0;
|
|
|
|
// #617: план уходит по HTTP (`/api/houseplan/plans/upload`), не по WS
|
|
c.hass = { ...c.hass, fetchWithAuth: async (url, init) => {
|
|
if (url !== '/api/houseplan/plans/upload') throw new Error('unexpected fetch ' + url);
|
|
// пока файл «загружается», прилетает чужая ревизия конфига
|
|
reloadDuringUpload++;
|
|
await c._reloadConfigOnly(true);
|
|
const spaceId = init.body.get('space_id');
|
|
return { ok: true, status: 200, json: async () => ({
|
|
ok: true, url: '/api/houseplan/content/plans/_/' + spaceId + '.png?v=42',
|
|
}) };
|
|
}, callWS: async (m) => {
|
|
if (m.type === 'houseplan/plan/set') { wsUploads++; throw new Error('plan/set must not be used'); }
|
|
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', blob: new Blob([new Uint8Array([1])]), aspect: 1.6, name: 'a.png' } };
|
|
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.sentPlanAspect = sentF1?.plan_aspect; // the IMAGE's ratio; the canvas is square
|
|
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', blob: new Blob([new Uint8Array([2])]), aspect: 0.8, name: 'b.png' } };
|
|
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.atticPlanAspect = attic?.plan_aspect;
|
|
out.wsUploads = wsUploads;
|
|
return out;
|
|
});
|
|
// зафиксировано прогоном на v1.44.8 и сверено с кодом
|
|
checkAll(res, {
|
|
reloadHappened: true,
|
|
sentPlanUrl: '/api/houseplan/content/plans/_/f1.png?v=42',
|
|
sentPlanAspect: 1.6,
|
|
sentTitle: 'Ground',
|
|
livePlanUrl: '/api/houseplan/content/plans/_/f1.png?v=42',
|
|
dialogClosed: true,
|
|
atticSaved: true,
|
|
atticHasPlan: true,
|
|
atticPlanAspect: 0.8,
|
|
wsUploads: 0,
|
|
});
|
|
await finish(browser);
|