mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix(plans): загрузка плана по HTTP, предел 8 МБ проверяется до отправки (#617)
План уходил 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
This commit is contained in:
@@ -3509,6 +3509,65 @@ const MUTANT_DEFINITIONS = [
|
||||
+ ' };',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-upload-client-limit',
|
||||
guard: 'node demo/smoke_plan_upload_limit.mjs',
|
||||
because: 'an SVG above the plan limit must be refused at pick time with a toast naming the '
|
||||
+ 'limit — otherwise it is staged and only fails after Save, or, on an old backend, closes '
|
||||
+ 'the socket (#617 AC2)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: " if (file.size > MAX_PLAN_BYTES) return refuse('toast.plan_too_large', { mb: MAX_PLAN_MB });\n",
|
||||
replace: ' // mutant: no client-side plan limit\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-upload-guard-original',
|
||||
guard: 'node demo/smoke_plan_upload_limit.mjs',
|
||||
because: 'above the plan limit the original cannot be stored — offering «Keep the original» '
|
||||
+ 'sends a file the server must refuse; only the reduced copy may be offered (#617 AC3)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: ' return renderBackdropGuard(host, apply, close, hass, undefined, size <= MAX_PLAN_BYTES, '
|
||||
+ 'MAX_PLAN_BYTES);',
|
||||
replace: ' return renderBackdropGuard(host, apply, close, hass, undefined, true, MAX_PLAN_BYTES);',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-upload-reduced-over-limit-staged',
|
||||
guard: 'node demo/smoke_plan_upload_limit.mjs',
|
||||
because: 'a reduced copy of a huge scan can still exceed the plan limit; staging it defers '
|
||||
+ 'the refusal to a 413 after Save instead of saying so at once (#617 AC3)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: ' if (planLimitBytes !== undefined && out.blob.size > planLimitBytes) {',
|
||||
replace: ' if (planLimitBytes !== undefined && out.blob.size < 0) { // mutant: never over',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-upload-413-text-dropped',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/plan-upload-limit.test.mjs',
|
||||
because: 'a 413 must read as «file larger than 8 MB», not «too_large» or «HTTP 413»; the '
|
||||
+ 'user has to learn the limit from the error (#617 AC5)',
|
||||
patches: [{
|
||||
file: 'src/backdrop-pick.ts',
|
||||
find: " too_large: t('err.too_large', { mb: json.max_mb || MAX_PLAN_MB }),\n",
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-upload-server-bound',
|
||||
guard: 'python3 -m pytest tests_backend/test_plan_upload.py -q -p no:cacheprovider',
|
||||
because: 'the streaming bound is inclusive: MAX_PLAN_BYTES passes and one byte more is refused '
|
||||
+ 'before the body is buffered whole; a bound that slips by a chunk stores an oversized '
|
||||
+ 'plan (#617 AC4)',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/plans.py',
|
||||
find: ' if size > limit:\n return None\n',
|
||||
replace: ' if size > limit + chunk:\n return None\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'cold-view-vacuum-mapid-delegated',
|
||||
guard: 'node demo/smoke_cold_view_vacuum.mjs',
|
||||
|
||||
@@ -305,6 +305,20 @@ export const SMOKE_LINKS = [
|
||||
+ 'with alpha preserved, the cancel-only hard dialog, the honest phase-2 failure (toast, '
|
||||
+ 'clean staging, no silent original upload) and the SVG bypass (#39)',
|
||||
},
|
||||
{
|
||||
symbols: [
|
||||
'stagePlanFile', 'uploadPlanFile', 'renderPlanBackdropGuard',
|
||||
'MAX_PLAN_BYTES', 'MAX_PLAN_MB', 'PLAN_UPLOAD_PATH',
|
||||
],
|
||||
smokes: [
|
||||
'smoke_plan_upload_limit.mjs', 'smoke_plan_upload_race.mjs',
|
||||
'smoke_plan_upload_reject.mjs', 'smoke_backdrop_guard.mjs',
|
||||
],
|
||||
because: '#617: the smokes drive _pickPlanFile/_saveSpaceDialog of both runtimes on the '
|
||||
+ 'production bundle and observe the transport (one multipart POST, zero plan/set), the '
|
||||
+ 'pre-send limit toast, the reduced-copy-only guard and the 413 text; none of them names '
|
||||
+ 'the shared helpers, so a name search cannot find them',
|
||||
},
|
||||
{
|
||||
symbols: ['vacMapIdWithFallback', 'vacMapIdFromAttrs', 'readVacTelemetry'],
|
||||
smokes: ['smoke_cold_view_vacuum.mjs'],
|
||||
|
||||
Reference in New Issue
Block a user