mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
@@ -0,0 +1,420 @@
|
||||
# ТЗ #423 — Полиш support pipeline и защитных инструментов v1.70.0
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/423
|
||||
- Приоритет: P3, `tech-debt` / `polish`
|
||||
- Маршрут: full; затронуты backend, frontend UX/i18n, transport, bundle graph и
|
||||
Playwright tooling, поэтому задача не проходит критерий одной поверхности
|
||||
лёгкого трека
|
||||
- Связанные контракты: #43 (Help & feedback), #352/#367 (bundle ownership и
|
||||
бюджет), #404/#407/#421 (browser exception guard), #422 (docs screenshot gate)
|
||||
|
||||
## Сценарий
|
||||
|
||||
Пользователь обновляет карточку и интеграцию House Plan через HACS. Браузер ещё
|
||||
может держать предыдущий bundle, либо одна из двух частей обновляется раньше
|
||||
другой. Пользователь открывает «Помощь и обратную связь», пишет сообщение и при
|
||||
желании прикладывает обезличенный пакет.
|
||||
|
||||
Одновременно разработчик должен иметь честные сведения о Repair families,
|
||||
дешёвый отказ при исчерпанном лимите preview, безопасное имя файла, контролируемый
|
||||
initial bundle и benchmark, который не пропускает browser exception.
|
||||
|
||||
## Что человек увидит до и после
|
||||
|
||||
**До:** при любом несовпадении номера релиза форма полностью скрывается, даже
|
||||
если обе стороны поддерживают один и тот же support API. Скачанный JSON и
|
||||
multipart-вложение названы префиксом живого preview-token. Остальные дефекты
|
||||
почти не видны напрямую: новый тип Repair может отсутствовать в пакете, а запрос
|
||||
с исчерпанным лимитом сначала зря строит пакет до 8 МиБ.
|
||||
|
||||
**После:** форма доступна при совместимом `support_api`, независимо от равенства
|
||||
номеров релиза. Старый backend без capability по-прежнему fail-closed показывает
|
||||
предложение обновиться. И ручное скачивание, и relay attachment получают одно
|
||||
имя по короткому префиксу SHA-256, без части capability-token. Тексты, поля,
|
||||
согласие на вложение и отправка внешне не меняются.
|
||||
|
||||
## Цель
|
||||
|
||||
Закрыть шесть подтверждённых разрывов аудита v1.70.0 без изменения support
|
||||
package v1 и без расширения передаваемых приватных данных:
|
||||
|
||||
1. перечислять все активные стабильные семейства House Plan Repair;
|
||||
2. не использовать capability-token в имени файла;
|
||||
3. отклонять очевидно лишний preview до дорогой сборки;
|
||||
4. согласовывать форму по версии support API, а не версии релиза;
|
||||
5. вынести form-only локализации из initial View graph;
|
||||
6. подключить backdrop decode benchmark к общему browser-error guard.
|
||||
|
||||
## Проблема и подтверждённые причины
|
||||
|
||||
### 1. Repair families
|
||||
|
||||
`_support_repairs()` сейчас анализирует только `issue_id.startswith("broken_plan_")`
|
||||
и жёстко выдаёт `code: "broken_plan"`. Реестр уже хранит безопасный стабильный
|
||||
`translation_key`; именно он описывает семейство, тогда как issue id содержит
|
||||
идентичность конкретного экземпляра и может включать приватный placeholder.
|
||||
|
||||
### 2. Filename
|
||||
|
||||
`ws_support_submit()` передаёт preview-token в `filename_token`, а
|
||||
`support_transport.py` пишет первые 32 символа в multipart filename. Frontend
|
||||
использует первые 12 символов того же token при Download JSON. Полный token не
|
||||
раскрывается, но имя без необходимости основано на bearer capability.
|
||||
|
||||
### 3. Preview quota
|
||||
|
||||
`ws_support_preview()` захватывает копии store и исполняет `_build_snapshot()` до
|
||||
prune/count. Только после потенциальной валидации, псевдонимизации и сериализации
|
||||
8 МиБ приходит `support_rate_limited`.
|
||||
|
||||
### 4. Compatibility
|
||||
|
||||
`_buildSupportPreview()`, `_submitSupport()` и `_renderSupportDialog()` требуют
|
||||
`_haIntegrationVersion === CARD_VERSION`. Номер релиза является диагностикой,
|
||||
но не версией протокола. Владелец выбрал явную capability `support_api: 1`.
|
||||
|
||||
### 5. Lazy support copy
|
||||
|
||||
43 английских `support.*` и соответствующие русские строки входят в общий
|
||||
initial chunk через eager locale dictionaries. Только `support.title` нужен
|
||||
карточке до загрузки editor runtime; вся остальная форма уже lazy.
|
||||
|
||||
### 6. Backdrop benchmark
|
||||
|
||||
`demo/benchmark_backdrop_decode.mjs` создаёт `page` напрямую и не вызывает ни
|
||||
`watchPage()`, ни `reportPageErrors()`. Необработанный `pageerror` может попасть
|
||||
в лог, но оставить exit code нулевым.
|
||||
|
||||
## Скоуп
|
||||
|
||||
В скоупе:
|
||||
|
||||
- безопасная агрегация Repair issues по `translation_key`;
|
||||
- одинаковый short-id из SHA-256 для browser download и multipart filename;
|
||||
- quota preflight до store copy/build и повторная проверка после асинхронной
|
||||
сборки;
|
||||
- top-level capability `support_api` в ответе `houseplan/config/get`;
|
||||
- component-memory состояние capability и единый чистый predicate совместимости;
|
||||
- lazy support dictionaries для RU/EN/DE/FR с прежними значениями строк;
|
||||
- bundle ownership test и зафиксированное уменьшение initial View gzip;
|
||||
- подключение backdrop benchmark к `watchPage()` и `reportPageErrors()`;
|
||||
- unit/backend/smoke/contract tests, User Guide и changelog RU/EN.
|
||||
|
||||
## Не-скоуп
|
||||
|
||||
- изменение endpoint relay, consent, retention, формы, package schema или набора
|
||||
приватных данных;
|
||||
- изменение лимитов `3/3`, TTL 10 минут или replacement semantics;
|
||||
- отправка списка raw Repair ids, placeholders, titles или exception text;
|
||||
- новый negotiation endpoint или пробный submit ради определения capability;
|
||||
- ленивый вынос всей RU/EN локали: переносится только семейство support form;
|
||||
- изменение screenshot capture workflow: неверный комментарий и межпрогонный
|
||||
гейт уже принадлежат #422;
|
||||
- превращение backdrop benchmark в обязательный CI performance gate.
|
||||
|
||||
## UX
|
||||
|
||||
- About и Guide доступны всегда, как сейчас.
|
||||
- Форма, checkbox и Send доступны, когда `support_api === 1`.
|
||||
- Отсутствующее, нецелое, нулевое или неизвестное значение capability считается
|
||||
несовместимым. UI показывает локализованное сообщение об обновлении до
|
||||
совместимых версий, не технический номер API.
|
||||
- Несовпадающие card/integration release versions сами по себе больше ничего не
|
||||
скрывают и не блокируют.
|
||||
- Никаких новых действий, переключателей, toast, фокусов и состояний загрузки.
|
||||
- Download JSON сохраняет те же bytes, но имя становится
|
||||
`houseplan-support-{sha256[0:12]}.json`.
|
||||
|
||||
## Контракт реализации
|
||||
|
||||
### 1. Safe Repair family
|
||||
|
||||
Для каждой активной записи реестра с `domain === houseplan` берётся
|
||||
`translation_key` объекта issue. Значение включается только если это строка,
|
||||
соответствующая `^[a-z][a-z0-9_]{0,63}$`. Записи без безопасного ключа
|
||||
пропускаются fail-closed.
|
||||
|
||||
Одинаковые ключи агрегируются, результат сортируется по `code` и имеет прежний
|
||||
вид `{ "code": string, "count": positive integer }`. Ни `issue_id`, ни
|
||||
translation placeholders, ни display text в package не попадают. Два
|
||||
`broken_plan_<space>` по-прежнему дают `{code: "broken_plan", count: 2}`;
|
||||
будущее семейство с другим id автоматически попадает под своим translation key.
|
||||
|
||||
### 2. Filename short-id
|
||||
|
||||
Short-id равен первым 12 lowercase hex символам уже рассчитанного SHA-256 exact
|
||||
attachment bytes. SHA уже входит в preview и request metadata; отдельный random
|
||||
id и новое поле WebSocket ответа не нужны. Благодаря случайному namespace
|
||||
псевдонимов новый preview имеет новые bytes/hash даже для того же плана.
|
||||
|
||||
- frontend Download использует `preview.sha256.slice(0, 12)`;
|
||||
- backend transport строит имя только из валидированного
|
||||
`attachment_sha256[:12]`;
|
||||
- parameter `filename_token` удаляется из transport API;
|
||||
- при `attachment is None` filename не создаётся;
|
||||
- browser и relay получают одинаковое имя.
|
||||
|
||||
### 3. Двухфазная quota-проверка
|
||||
|
||||
Один helper проверяет вместимость preview-map для `(owner, draft_id)`:
|
||||
|
||||
1. prune expired records по одному captured monotonic `now`;
|
||||
2. при подсчёте временно исключить старую запись того же owner/draft, потому что
|
||||
успешный refresh заменит её;
|
||||
3. проверить per-user и total limits;
|
||||
4. при отказе отправить `support_rate_limited` до чтения store, deep copy и
|
||||
`async_add_executor_job(_build_snapshot)`.
|
||||
|
||||
Старый preview текущего draft на preflight не удаляется: неудачная новая сборка
|
||||
не должна уничтожать пригодный снимок. После возврата executor выполняются новый
|
||||
prune и тот же capacity check, потому что во время `await` другой запрос мог
|
||||
занять слот. Только после успешной второй проверки старый token того же draft
|
||||
удаляется и новый record записывается без следующего `await`.
|
||||
|
||||
### 4. Версия support API
|
||||
|
||||
Текущий backend добавляет в каждый полный и projected ответ
|
||||
`houseplan/config/get` поле:
|
||||
|
||||
```json
|
||||
{ "support_api": 1 }
|
||||
```
|
||||
|
||||
Поле — runtime capability, а не persisted config. Миграции нет. Карточка на
|
||||
каждом успешном `config/get` нормализует значение: только safe integer `1`
|
||||
считается поддерживаемым; отсутствие/другая версия сбрасывает прошлое значение,
|
||||
чтобы downgrade backend не оставил stale permission.
|
||||
|
||||
Чистый helper `supportApiCompatible(value)` используется всеми тремя путями:
|
||||
render, preview build и submit. Release `integration_version` сохраняется для
|
||||
диагностики и других функций, но больше не участвует в support form gate.
|
||||
Добавочные изменения протокола остаются в API v1; breaking change получает новый
|
||||
номер и старый frontend fail-closed.
|
||||
|
||||
### 5. Lazy support dictionaries
|
||||
|
||||
В основных `src/i18n/{en,ru,de,fr}.json` остаётся `support.title`, потому что
|
||||
header action существует до загрузки editor runtime. Остальные `support.*`
|
||||
переносятся без изменения текста и placeholders в отдельные словари, импортируемые
|
||||
только графом `houseplan-editor-runtime`.
|
||||
|
||||
Lazy helper:
|
||||
|
||||
- типизирует собственный набор support keys;
|
||||
- выбирает точный словарь RU/EN/DE/FR и синхронно fallback-ит в English;
|
||||
- использует общий `subst` для placeholders;
|
||||
- не мутирует глобальный LanguageRuntime и не создаёт дополнительной сетевой
|
||||
загрузки после загрузки editor runtime.
|
||||
|
||||
Общие i18n tests отдельно доказывают равенство ключей/placeholders основного и
|
||||
support-наборов во всех четырёх языках. Dead-key scan рассматривает оба английских
|
||||
словаря. Production build обязан показать, что form-only English/Russian marker
|
||||
strings отсутствуют в `initialViewFiles` и присутствуют в `lazyEditorFiles`.
|
||||
|
||||
Baseline для сравнения берётся из чистого `dev` на начале задачи: **291046 B
|
||||
gzip initial View**. После реализации значение должно быть строго меньше baseline;
|
||||
увеличение бюджета `300000` или простая рекалибровка запрещены.
|
||||
|
||||
### 6. Browser error guard benchmark
|
||||
|
||||
Benchmark оборачивает созданную страницу штатным `watchPage()`. После всех cases,
|
||||
но до `browser.close()`, он вызывает `await reportPageErrors()`; при true
|
||||
завершает сценарий ненулевым кодом без печати ложного success. Синхронные ошибки
|
||||
отдельных слишком больших cases продолжают печататься как измерительный `FAIL` и
|
||||
не превращаются автоматически в crash всего benchmark: предмет guard — только
|
||||
необработанный page error.
|
||||
|
||||
Contract test охватывает все `demo/benchmark_*.mjs`, которые создают Playwright
|
||||
page: такая страница должна пройти через `watchPage`, а скрипт — запросить
|
||||
`reportPageErrors` или `finish`. Отрицательная проверка удаляет один из этих
|
||||
вызовов и обязана краснеть. Для динамического доказательства сам benchmark
|
||||
получает служебный `--guard-probe`: режим не запускает тяжёлую decode-матрицу,
|
||||
создаёт контролируемый tail `pageerror`, проходит через тот же финальный verdict
|
||||
и обязан завершиться кодом 1 с сообщением общего guard. Обычный запуск без флага
|
||||
не меняет cases или формат строк измерений.
|
||||
|
||||
## Модель данных, API и миграция
|
||||
|
||||
- Persisted config/layout, model version, import/export и support package v1 не
|
||||
меняются.
|
||||
- `support_api` — новое additive top-level поле response `config/get`; старые
|
||||
клиенты его игнорируют, старые backends не присылают.
|
||||
- `_haSupportApi` живёт только в памяти экземпляра карточки, в cache/localStorage
|
||||
не записывается.
|
||||
- Preview record сохраняет прежние bytes/hash/versions. Нового filename id в
|
||||
record нет.
|
||||
- Relay request JSON и validation schema не меняются; меняется только безопасное
|
||||
multipart filename.
|
||||
|
||||
## i18n
|
||||
|
||||
Видимый текст `support.update_required` обновляется во всех RU/EN/DE/FR: вместо
|
||||
требования одинакового номера релиза он просит обновить карточку и интеграцию до
|
||||
совместимых версий. Остальные строки переносятся побайтово без правки смысла.
|
||||
|
||||
Основные ключи:
|
||||
|
||||
- `support.title` — остаётся в core dictionary;
|
||||
- `support.update_required` — lazy, новый смысл compatibility;
|
||||
- все остальные `support.*` — lazy, значения без изменения.
|
||||
|
||||
## Accessibility, touch и kiosk
|
||||
|
||||
Разметка dialog, порядок tab, label/aria-live, focus, 44×44 targets и responsive
|
||||
layout не меняются. Touch, desktop и keyboard используют один compatibility
|
||||
predicate. Kiosk по-прежнему скрывает Help action. Вынос словарей не допускает
|
||||
пустого текста или English flash: editor runtime и его словари загружаются одной
|
||||
lazy graph dependency до открытия dialog.
|
||||
|
||||
## Security и privacy
|
||||
|
||||
- Capability token остаётся только в WebSocket payload/runtime-map и никогда не
|
||||
входит в filename.
|
||||
- SHA short-id не добавляет новую информацию: полный SHA уже явно показан
|
||||
пользователю и отправляется relay для проверки bytes.
|
||||
- Repair aggregation принимает только стабильный безопасный translation key;
|
||||
сырые ids/placeholders исключены тестом.
|
||||
- Capability negotiation не вызывает relay и не ослабляет HA `may_write`.
|
||||
- Quota preflight уменьшает доступную злоумышленнику дорогую работу; финальная
|
||||
проверка сохраняет ограничение при конкурентных запросах.
|
||||
|
||||
## Производительность
|
||||
|
||||
- Исчерпанная preview quota отклоняется до deep copy/schema/projection/JSON.
|
||||
- Обычный допустимый preview получает два линейных прохода по максимум трём
|
||||
records; стоимость ничтожна относительно snapshot build.
|
||||
- Initial View gzip обязан уменьшиться относительно 291046 B. Lazy editor может
|
||||
вырасти на перенесённые строки; суммарные bytes допустимы, потому что платит
|
||||
только открывающий редактор/Help пользователь.
|
||||
- Backdrop benchmark не становится CI gate и не меняет измеряемую матрицу; один
|
||||
финальный browser round-trip не входит в case timing.
|
||||
|
||||
## Затрагиваемые файлы
|
||||
|
||||
Ожидаемый набор:
|
||||
|
||||
- `custom_components/houseplan/websocket_api.py`;
|
||||
- `custom_components/houseplan/support_transport.py`;
|
||||
- `tests_backend/test_ha_websocket.py`;
|
||||
- `tests_backend/test_ha_support_transport.py`;
|
||||
- `src/houseplan-card.ts`, `src/houseplan-editor-runtime.ts`;
|
||||
- `src/support-feedback.ts` либо отдельный compatibility helper;
|
||||
- `src/i18n/{en,ru,de,fr}.json` и новые lazy support dictionaries/helper;
|
||||
- `test/support-feedback.test.mjs`, `test/i18n.test.mjs`,
|
||||
`test/i18n-dead-keys.test.mjs`, `test/bundle-assets.test.mjs`;
|
||||
- `demo/smoke_support_feedback.mjs`;
|
||||
- `demo/benchmark_backdrop_decode.mjs` и его contract test;
|
||||
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`;
|
||||
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`.
|
||||
|
||||
`.github/workflows/docs-screenshots.yml` не меняется в #423.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- **AC1.** Два House Plan issues одного безопасного `translation_key` дают одну
|
||||
family/count; другое безопасное семейство попадает автоматически; foreign
|
||||
domain и unsafe/missing key не попадают, raw ids/placeholders отсутствуют.
|
||||
Доказательство: backend unit/HA test + code review.
|
||||
- **AC2.** Browser download и relay multipart используют exact filename
|
||||
`houseplan-support-{sha256[0:12]}.json`; capability-token отсутствует в имени,
|
||||
bytes/hash не меняются. Доказательство: frontend smoke + transport test.
|
||||
- **AC3.** При исчерпанной quota без replaceable token ответ
|
||||
`support_rate_limited` приходит без store loads/executor build; refresh своего
|
||||
draft имеет право заменить token и не удаляет старый при build failure.
|
||||
Доказательство: HA backend tests.
|
||||
- **AC4.** Если слот занят конкурентно во время snapshot `await`, финальная
|
||||
проверка не превышает лимит и не удаляет прежний token текущего draft.
|
||||
Доказательство: управляемый backend test.
|
||||
- **AC5.** `config/get` всегда возвращает `support_api: 1`, включая projection;
|
||||
поле не хранится в config. Старый backend без поля остаётся совместим с
|
||||
frontend load, но support form fail-closed. Доказательство: backend + unit.
|
||||
- **AC6.** Support form/preview/submit работают при разных release versions и
|
||||
`support_api === 1`; missing, invalid, 0 или 2 блокируют все три пути одним
|
||||
predicate. Доказательство: unit + `smoke_support_feedback.mjs`.
|
||||
- **AC7.** Все RU/EN/DE/FR support keys непусты, имеют одинаковый key/placeholder
|
||||
set и отображают прежний текст, кроме согласованного compatibility message.
|
||||
Core содержит только `support.title`; form-only strings принадлежат lazy editor
|
||||
graph. Доказательство: i18n + bundle ownership tests.
|
||||
- **AC8.** Production build имеет initial View gzip строго меньше baseline
|
||||
291046 B без изменения budget; Help dialog на четырёх языках остаётся полным.
|
||||
Доказательство: build manifest + smoke/code review.
|
||||
- **AC9.** Backdrop benchmark регистрирует page через `watchPage`, запрашивает
|
||||
финальный browser-error verdict и в `--guard-probe` выходит ненулевым при
|
||||
injected pageerror без запуска decode-матрицы. Обычный режим сохраняет прежние
|
||||
cases/вывод. Доказательство: contract test + динамическая отрицательная probe.
|
||||
- **AC10.** `.github/workflows/docs-screenshots.yml` не меняется; #423 ссылается
|
||||
на #422 как владельца дублирующего пункта. Доказательство: diff/code review.
|
||||
- **AC11.** Typecheck, unit, build и затронутые backend tests зелёные; runtime
|
||||
package/config schemas и UI layout не меняются. Доказательство: локальный цикл
|
||||
и Linux CI.
|
||||
- **AC12.** User Guide и оба changelog описывают capability-based compatibility
|
||||
и безопасное имя без технических обещаний о внутреннем SHA-префиксе сверх
|
||||
нужного пользователю. Доказательство: docs guard/code review.
|
||||
|
||||
## План отрицательной проверки
|
||||
|
||||
1. Вернуть агрегацию по `broken_plan_` — тест с новым `translation_key` краснеет.
|
||||
2. Вернуть filename от token — smoke/transport test находят token prefix.
|
||||
3. Перенести preflight после executor либо убрать post-await check — отдельные
|
||||
quota tests краснеют на build call или превышении лимита.
|
||||
4. Вернуть сравнение `integration_version === CARD_VERSION` — mixed-release
|
||||
smoke краснеет; удалить/reset `support_api` adoption — old-backend case
|
||||
краснеет.
|
||||
5. Вернуть form strings в core dictionary — bundle ownership test находит marker
|
||||
в initial graph и initial gzip перестаёт уменьшаться.
|
||||
6. Удалить `watchPage` или browser-error verdict из benchmark — contract/probe
|
||||
краснеет.
|
||||
|
||||
## Риски
|
||||
|
||||
- **Stale capability после downgrade.** Missing/invalid field явно сбрасывает
|
||||
состояние на incompatible при каждом `config/get`.
|
||||
- **API v2 ошибочно принимается старой карточкой.** Predicate принимает ровно 1;
|
||||
additive изменения не повышают API, breaking change повышает.
|
||||
- **Quota race.** Capacity проверяется повторно после каждого `await`, запись
|
||||
replacement выполняется атомарным синхронным участком.
|
||||
- **Неудачный refresh уничтожает старый preview.** Старый token удаляется только
|
||||
после успешного build и финальной capacity-проверки.
|
||||
- **Repair key раскрывает данные.** Строгая форма ключа и запрет fallback к issue
|
||||
id оставляют только проектный translation key.
|
||||
- **Lazy copy теряет локаль/fallback.** Отдельная parity/placeholder matrix и
|
||||
smoke четырёх языков блокируют неполный перенос.
|
||||
- **Экономия бандла мнимая.** AC фиксирует owner graph и числовой baseline, не
|
||||
только общий budget.
|
||||
- **Benchmark начинает считать ожидаемый per-case FAIL как exception.** Guard
|
||||
реагирует только на pageerror; существующий catch/вывод cases сохраняется.
|
||||
|
||||
## Откат
|
||||
|
||||
Откат implementation-коммита возвращает строгий release-version gate, прежние
|
||||
filenames, eager support strings, позднюю quota-проверку и старый benchmark.
|
||||
Persisted data и package schema откатывать не нужно. Backend additive
|
||||
`support_api` безопасно удалить вместе с frontend predicate одним релизом;
|
||||
старый frontend и так игнорирует поле.
|
||||
|
||||
## Release-артефакты
|
||||
|
||||
- обязательны `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же
|
||||
User-Visible commit;
|
||||
- обязательна правка раздела Help & feedback в обоих User Guide;
|
||||
- golden/docs screenshots не нужны: DOM/layout/text видимой совместимой формы не
|
||||
меняются, а compatibility message проверяется текстом/smoke;
|
||||
- performance artifact — before/after `dist/houseplan-assets.json` с baseline
|
||||
initial gzip 291046 B и доказательством ownership;
|
||||
- security evidence — transport test, где filename не содержит token;
|
||||
- issue остаётся открытым в `S8-merged` до пакетного закрытия при выпуске беты.
|
||||
|
||||
## Принятые предположения
|
||||
|
||||
- Владелец подтвердил default: protocol capability важнее равенства product
|
||||
versions; первая и единственная поддерживаемая версия сейчас равна 1.
|
||||
- SHA-256 — уже раскрытая внутри consented preview метаинформация и допустимый
|
||||
источник short-id; новое случайное поле не добавляется.
|
||||
- `translation_key` — каноническое имя Repair family. Repair без безопасного
|
||||
translation key считается внутренне некорректным и в support package не
|
||||
показывается.
|
||||
- Все form-only support strings переносятся для четырёх локалей одновременно;
|
||||
оставлять RU eager при выносе только EN было бы скрытой архитектурной
|
||||
асимметрией.
|
||||
- Пункт issue про порядок комментария в docs workflow полностью принадлежит
|
||||
#422 и не расходует реализацию/ревью #423.
|
||||
@@ -175,6 +175,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#200](https://github.com/Matysh/houseplan-card/issues/200) Одинаковая геометрия подписи комнаты в View и Plan editor | [200-room-label-parity.md](200-room-label-parity.md) |
|
||||
| [#340](https://github.com/Matysh/houseplan-card/issues/340) Обязательная ревизия повторной записи config/set | [340-config-set-revision.md](340-config-set-revision.md) |
|
||||
| [#372](https://github.com/Matysh/houseplan-card/issues/372) Компактное верхнее кадрирование static card без заголовка | [372-space-card-empty-title.md](372-space-card-empty-title.md) |
|
||||
| [#423](https://github.com/Matysh/houseplan-card/issues/423) Полиш support pipeline и защитных инструментов v1.70.0 | [423-v170-polish.md](423-v170-polish.md) |
|
||||
|
||||
## P3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user