docs: #39 spec revision 3 per SPEC-REVIEW-39-r1

Issue: #39
User-Visible: no
This commit is contained in:
Codex
2026-08-29 09:15:21 +03:00
parent b3ed32d2de
commit f787de997b
+86 -7
View File
@@ -2,7 +2,7 @@
- Issue: https://github.com/Matysh/houseplan-card/issues/39
- Приоритет: P3 (триаж владельца 2026-08-15), полный трек
- Ревизия: 2 (2026-08-29) — research-фаза проведена, константы предложены по бенчмарку
- Ревизия: 3 (2026-08-29) — по SPEC-REVIEW-39-r1: сценарий, i18n-ключи, риски, release-артефакты, двухфазный hard
## Цель
@@ -10,6 +10,21 @@
память планшета/WebView, и предлагать уменьшенную копию — не трогая текущий
план и оригинал файла при любой ошибке.
## Сценарий
Владелец дома настраивает план с планшета или ноутбука: в диалоге пространства
выбирает «Файл» и указывает скан поэтажного плана — обычно это фото или скан
150–600 DPI, который «как есть» весит десятки мегапикселей. Сегодня такой выбор
молча вешает вкладку на слабом планшете (полный decode + тройная конвертация в
base64 до каких-либо проверок).
**До**: выбрал большой файл → вкладка замирает или падает без объяснений;
план, который был на экране, можно потерять вместе с вкладкой.
**После**: выбрал большой файл → мгновенно появляется понятный диалог «Файл
очень большой (10000×10000, 58 МБ, потребует ~380 МБ памяти)» с кнопкой
«Загрузить уменьшенную копию»; одно нажатие — и на плане та же картинка, но
безопасного размера. Текущий план в любом исходе остаётся цел.
## Текущее состояние (анкеры кода)
`_pickPlanFile` (`houseplan-editor-runtime.ts:8243`) принимает PNG/JPEG/WebP/SVG
@@ -76,15 +91,49 @@ fail-closed к диалогу, не к тихому продолжению. SVG
- `warn` — диалог (`hp-dialog`, паттерн существующих подтверждений): разрешение,
размер файла, оценка decoded-памяти; действия «Загрузить уменьшенную копию»
(основное), «Оставить оригинал», «Отмена». Числа — из probe, до decode.
- `hard` (сторона > 16384, decode-ошибка или таймаут 10 с на этапе
уменьшения) — только «Отмена» + совет уменьшить файл на десктопе; текущая
подложка и staging не меняются.
- `hard` — ДВЕ фазы с разным моментом наступления и одним исходом
(«ничего не изменилось»):
- **фаза 1, синхронная** (сторона > 16384 по заголовку, до какого-либо
decode): диалог `backdrop.too_large_*` с единственной кнопкой «Отмена» и
советом уменьшить файл на десктопе;
- **фаза 2, асинхронная** (пользователь выбрал «Уменьшенную копию», а decode
упал или не уложился в таймаут 10 с): warn-диалог закрывается, показывается
тост `backdrop.downscale_failed`; поле выбора файла сброшено, staging
чист — пользователь может выбрать «Оставить оригинал», повторив выбор
файла, либо уменьшить файл сам. Автоматического фолбэка на оригинал нет:
молча грузить то, от чего пользователь только что отказался, нечестно.
- Уменьшение: `createImageBitmap(file, {imageOrientation: 'from-image'})` (EXIF
учтён) → OffscreenCanvas (fallback `<canvas>`), aspect сохраняется; alpha из
probe: PNG/WebP c alpha → PNG, opaque → JPEG q0.9 (WebP-энкод не берём —
Safari не пишет). Уменьшенный Blob идёт тем же путём planFile → upload →
квота/copy-on-write, что и оригинал.
- Тексты en/ru/de; диалог на мобильной ширине без горизонтального скролла.
- Диалог на мобильной ширине без горизонтального скролла.
## i18n (ключи en/ru; de — перевод тех же ключей)
- `backdrop.large_title` — "Large image" / «Большое изображение»;
- `backdrop.large_body` — "This image is {w}×{h} ({fileMb} MB file) and needs
about {decodedMb} MB of memory to display. On tablets this can crash the
page." / «Изображение {w}×{h} ({fileMb} МБ), для показа потребуется около
{decodedMb} МБ памяти. На планшетах это может привести к падению страницы.»;
- `backdrop.unknown_body` — "The image dimensions could not be read — the file
may be damaged. Continuing may crash the page on tablets." / «Не удалось
прочитать размеры изображения — файл может быть повреждён. Продолжение может
привести к падению страницы на планшете.»;
- `backdrop.use_downscaled` — "Upload a reduced copy" / «Загрузить уменьшенную
копию»;
- `backdrop.keep_original` — "Keep the original" / «Оставить оригинал»;
- `backdrop.too_large_title` — "Image is too large" / «Изображение слишком
большое»;
- `backdrop.too_large_body` — "This image exceeds what browsers can display
({w}×{h}, limit {limit} px per side). Please reduce it in a desktop editor
and upload again." / «Изображение превышает возможности браузера ({w}×{h},
предел {limit} px по стороне). Уменьшите его в редакторе на компьютере и
загрузите снова.»;
- `backdrop.downscale_failed` — "Could not create the reduced copy. The
original plan was not changed." / «Не удалось создать уменьшенную копию.
Текущий план не изменён.»;
- `toast.plan_formats` — существующий, без изменений.
## Транзакция (без изменений, закрепляется тестом)
@@ -103,8 +152,11 @@ issue = оригинальный ФАЙЛ пользователя никогд
2. «Уменьшенная копия»: длинная сторона 4096, aspect сохранён; PNG с alpha →
PNG (alpha жива), opaque → JPEG; результат уходит существующим upload-путём.
3. «Оставить оригинал» — прежнее поведение байт-в-байт.
4. `hard`: сторона >16384 либо decode-fail/таймаут — только Отмена; текущая
подложка, staging и конфиг не изменились.
4. `hard` фаза 1: сторона >16384 из заголовка — диалог «слишком большое» с
одной «Отменой», без decode; подложка/staging/конфиг не изменились.
4б. `hard` фаза 2: decode-fail или таймаут 10 с после выбора «Уменьшенной
копии» — тост `backdrop.downscale_failed`, staging чист, подложка/конфиг не
изменились, повторный выбор файла работает.
5. SVG: только прежняя валидация, растеризации нет.
6. Битые/усечённые заголовки (PNG без IHDR, JPEG без SOF, WebP-огрызок) →
`unknown` → warn-диалог без чисел; продолжение возможно только явным выбором.
@@ -126,6 +178,33 @@ window.createImageBitmap). Мутанты реестра: (1) probe всегда
`hard` понижен до warn — смок красный; (4) staging не чистится при отказе —
тест транзакции красный.
## Риски
- **Пороги не с reference-планшета**: калибровка десктопная с консервативным
запасом; ложные срабатывания дешевы (лишний диалог), пропуски дороже — при
полевых жалобах правится один модуль констант. Смягчение: числа печатаются в
диалоге, жалобу легко диагностировать по скриншоту.
- **Зоопарк заголовков** (прогрессивный JPEG, WebP VP8L, EXIF в необычном
месте): парсер обязан отвечать `unknown`, не бросать — fail-closed к
warn-диалогу; таблица юнит-фикстур покрывает все подвиды из спецификаций
форматов.
- **OffscreenCanvas недоступен** (старые WebView): фолбэк на `<canvas>` в том
же кадре; ветка покрыта юнитом через подмену глобала.
- **Regress существующего upload-пути**: safe-путь меняет только механизм
base64 (паритет-юнит AC9) — поведенчески байт-в-байт.
## Release-артефакты
- CHANGELOG.md + CHANGELOG.ru.md — одна запись (User-Visible: yes);
- docs/BACKDROP.md — раздел «Большие изображения» (пороги, поведение, совет);
- docs/USER-GUIDE.md + USER-GUIDE.ru.md — абзац в теме подложек;
- docs/TESTING.md — чек-лист сценариев (warn/уменьшение/hard×2/SVG);
- i18n en/ru/de — ключи из раздела i18n (паритет-тесты стерегут);
- golden: не задевается (диалог не входит в golden-матрицу; скриншоты
документации пересъёмка capture по общему правилу src-правок);
- performance: бенчмарк-скрипт `demo/benchmark_backdrop_decode.mjs` в repo,
в перф-джобу CI НЕ добавляется (разовая калибровка, не регресс-гейт).
## Вне скоупа
Автопревращение SVG в растр; серверная перекодировка; изменение