mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
128 lines
23 KiB
Markdown
128 lines
23 KiB
Markdown
# CODE-REVIEW-437-r2
|
||
|
||
Issue: [#437](https://github.com/Matysh/houseplan-card/issues/437) — конфигурируемая read-only сводная панель поверх плана.
|
||
Этап: код-ревью, заход **r2**, блокирующих циклов израсходовано **1/4** до этого раунда (полный трек, лимит 4).
|
||
Материал этого раунда: `git diff 96e07b9a..89976c85` (`origin/dev..HEAD`, HEAD = `89976c85edc443da6a08ca0034eaa99d322f13d0`).
|
||
|
||
## Материал раунда r1 и объявление дельты (PROCESS §2.10)
|
||
|
||
- Вердикт r1: жёлтый, High 0 / Medium 4, документ `docs/reviews/CODE-REVIEW-437-r1.md`.
|
||
- Материал r1 назван в самом документе r1: `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD` на SHA `96e07b9a0255eef3a55cd6c3be08198a0adcc7ba`. SHA резолвится (не мёртвый), проверено `git show 96e07b9a --stat` — коммит существует в истории ветки.
|
||
- Дельта r1→r2: `git diff 96e07b9a..HEAD` — 4 коммита (`3c1a9f8f`, `af441149`, `336d8f30`, `89976c85`), 60 файлов в diffstat, из которых источник (`src/**`, `test/**`, `demo/**`, `docs/**`) — 11 файлов; остальное — сгенерированное (класс D, три копии бандла) и служебное (`tsconfig.test.json`).
|
||
- **Разбор полным не признан необходимым**: дельта не ребейзится на ушедший вперёд `dev` (родитель `HEAD` для `dev` — тот же `ed9ee026`, что и у материала r1), контракт AC1–AC26 не меняется, новая подсистема в буквальном смысле не заведена (`boot-soft-layout.ts` — извлечение уже существовавшей в `houseplan-card.ts` логики измерения шапки в отдельный файл, не новая возможность). Объём дельты (11 файлов источника, +366/−125 без сгенерированного) не сопоставим с объёмом исходной задачи (82 файла, +5131/−424). Разбор велся **по дельте** с одним отступлением: раздел «Проверено дополнительно» ниже — камера/refit-путь тронут дельтой напрямую (`_prepareCameraCommand`, `_refitView`, `_stagePointerDown`, `_bootSoftCancel`), поэтому прогнан более широкий, чем формальный diff, набор смоков именно по этому пути (см. «Как проверялось»), а не только `demo/smoke_summary_panel.mjs`.
|
||
|
||
## Закрытие раунда r1
|
||
|
||
| Находка r1 | Чем закрыта | Где это видно |
|
||
|---|---|---|
|
||
| **M1** — `demo/smoke_kiosk.mjs` падал (`TypeError: reading 'icon' of null`), т.к. `_saveKioskScale` больше не пишет в легаси-ключ `houseplan_card_kiosk_v1` | Смок переписан: сеет легаси-ключ заранее, читает новый per-instance ключ `houseplan.summary-panel.v1:*` и отдельно проверяет, что легаси-ключ не тронут | `demo/smoke_kiosk.mjs` (коммит `3c1a9f8f`, diff +14/−1); прогнан лично: `node demo/smoke_kiosk.mjs` → `OK`, `"persisted": true`, `"legacyScaleUntouched": true` |
|
||
| **M2** — защита «удалённое устройство не увеличивает счётчик» (Q2/AC18) без теста-свидетеля | Фикстура `test/summary-panel.test.mjs` расширена `removed`-маркерами (`d4` — удалённое устройство в области, `d5` — удалённый родитель с восстановленной дочерней сущностью); ожидаемый результат `['d1','d2','d5']` требует именно этой пары гардов | `test/summary-panel.test.mjs:169-203`; **воспроизведено лично** — см. таблицу «чем краснеет» ниже, тест реально падает при снятии гарда |
|
||
| **M3** — tap-target 34×34 CSS px против контракта ТЗ §5.4 (≥44×44); попутно найдено расхождение `TOUCH-SUPPORT.md` («панель + составной контрол» уже сузило формулировку) | `.summary-editor-row button`, `.summary-drag`, `.summary-switch`, `select`, `input[type=range]`, кнопки footer — все подняты до `min-width/height: 44px`; `TOUCH-SUPPORT.md` расширен на «и его простая форма настроек» | `src/summary-panel-style.ts` (диф `96e07b9a..3c1a9f8f`); `docs/TOUCH-SUPPORT.md` (диф `96e07b9a..HEAD`); witness `test/summary-panel.test.mjs` regex на `min-width:44px;...height:44px` — прогнан, зелёный |
|
||
| **M4** — AC21 (устойчивая идентичность карточки в Masonry) без теста-свидетеля, резолвер использовал «сырой» DOM-child-index на каждом уровне | Новый `src/summary-panel-identity.ts`: структурный путь до ближайшего `hui-card` вычисляется один раз и кешируется в `WeakMap` на сам DOM-узел обёртки — последующий реордер колонок Masonry возвращает старое значение из кеша, а не пересчитывает | `src/summary-panel-identity.ts` (новый файл); `test/summary-panel.test.mjs:145-167` («placement identity survives Masonry reflow and inner-card remount»), прогнан, зелёный. **Остаточное наблюдение, не блокирует** — см. Low L3 ниже |
|
||
|
||
Все четыре находки r1 закрыты по существу, с независимым воспроизведением, не только заявлением автора.
|
||
|
||
## Унаследовано из r1 (без повторной проверки)
|
||
|
||
Ниже — то, что дельта `96e07b9a..HEAD` не затрагивает, поэтому наследуется из `docs/reviews/CODE-REVIEW-437-r1.md` (материал SHA `96e07b9a`) без повторной проверки в этом раунде:
|
||
|
||
- структура диффа и границы SCOPE (backend `settings.summary_panel`, версионирование, change-aware проверка ссылок) — `custom_components/houseplan/validation.py`, `const.py`, `websocket_api.py` (кроме однострочной правки импорта в `af441149`, проверенной отдельно ниже);
|
||
- AC1, AC3–AC14, AC16, AC19, AC20, AC22–AC26 — доказательства этих AC лежат в файлах, не тронутых дельтой (`src/summary-panel-editor.ts`, `summary-panel-host.ts`, дизайн диалога, лимиты, i18n-словари, backend-хранение, kiosk-права кроме локального ключа);
|
||
- `pytest tests_backend/test_summary_panel.py` — 9 passed на `96e07b9a` (полный HA-harness недоступен и в этом окружении — см. «Чего не проверял»); дельта трогает backend только порядком импорта (`af441149`), не логику;
|
||
- `npm run golden:verify` полный прогон и `check-docs --screenshots` эталоны — не перезапускались, т.к. предрелizный гейт, не гейт ревью (PROCESS §8), а видимый результат панели дельтой не менялся (менялись только touch-размеры формы настроек и внутренняя логика identity/camera, не геометрия/цвета/раскладка панели).
|
||
|
||
## Как проверялось — гейты
|
||
|
||
Зелёного Validate на `89976c85` не найдено — прогнал сам.
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| Типы | `npx tsc --noEmit` | pass, 0 ошибок |
|
||
| Юниты (полный набор) | `npm test` | 2268 passed, 0 failed, 1 skipped (34.7s) |
|
||
| Сборка + бандл (3 копии) | `npm run build && npm run bundle:sync` | pass; `cmp` подтвердил байтовое совпадение `dist/houseplan-card.js` ↔ `custom_components/.../houseplan-card.js` ↔ `demo/srv/assets/houseplan-card.js` |
|
||
| Новый `any` | `node scripts/no-new-any.mjs --base 96e07b9a --head HEAD` | pass, 164 добавленные строки в 5 файлах, новых `any` нет |
|
||
| Документация (диф трогает `src/**`) | `node scripts/check-docs.mjs` | красный ожидаемо: отпечаток скриншотов устарел (правило #479, некритично на обычном push, обязательно перед beta candidate) |
|
||
| Бюджет бандла | `node scripts/bundle-budget.mjs` | pass, initial View 291 481 B при потолке 292 000±2000; уже учтённое предупреждение о запасе (#367), не новая находка |
|
||
| Процесс-гейт | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» (10 коммитов в диапазоне `origin/dev..HEAD`) |
|
||
| Выбор смоков по дельте r1→r2 | `node scripts/smoke-select.mjs --base 96e07b9a --head HEAD` | 43 прямых совпадения (камера/`_view`/`_zoom`/`_applyView` — широкая зона, ожидаемо для правки refit-пути) + 43 слабые связи |
|
||
| Целевые смоки (M1–M4 + прямая зона дельты) | `node demo/smoke_kiosk.mjs`, `smoke_summary_panel.mjs`, `smoke_houseplan_panel.mjs`, `smoke_danger_confirm_branches.mjs`, `smoke_room_fit.mjs` | все **OK** |
|
||
| Камера/refit смоки (дельта правит `_refitView`/`_applyView`/`_bootSoftCancel`) | `node demo/smoke_smooth_zoom.mjs`, `smoke_pan_any_zoom.mjs`, `smoke_zoom_out.mjs` | первые два **OK**; **`smoke_zoom_out.mjs` красный** — см. Medium M5 |
|
||
| Мутационная проверка защиты AC18 (M2) | ручная правка `test-build/summary-panel-metrics.js`: снял `removed.devices.has(device.id)` из первого цикла, вернул после проверки | тест **упал** (см. таблицу ниже), затем восстановлен, `git status` чист |
|
||
| Backend | `python -m pytest tests_backend -q` | **не прогонялся** — в этом окружении нет `.venv-backend` и системного HA; дельта трогает backend только порядком импорта (`af441149`), риск логики нулевой, наследуется прогон r1 (396 passed, 3 skipped) |
|
||
| Golden/invariants/perf | — | не прогонялись: дельта не меняет геометрию, `layout`, ссылки на пространства/толщины, видимый рендер панели или производительность — прогонять не по чему (PROCESS §8, «по необходимости») |
|
||
|
||
### Защитный AC — таблица «чем краснеет» (PROCESS §2.7, только для новых/переоткрытых в этом раунде)
|
||
|
||
| AC | Чем доказан | Чем краснеет |
|
||
|---|---|---|
|
||
| AC18 (устройства не дублируются, удалённые/восстановленные считаются верно) — M2 | `test/summary-panel.test.mjs` тест «device total counts unique…» | Снял `removed.devices.has(device.id)` из первого цикла `representedHaDeviceIds` → тест упал: ожидалось `['d1','d2','d5']`, получено `['d1','d2','d4','d5']` (утечка удалённого `d4`). Воспроизведено лично, файл восстановлен. |
|
||
|
||
## Находки
|
||
|
||
### Medium (в скоупе задачи, чинится в этой же задаче)
|
||
|
||
**M5 — регресс восстановления камеры View при возврате из редактора, унаследован из r1-материала и не закрыт дельтой.**
|
||
|
||
`demo/smoke_zoom_out.mjs` на `HEAD` (`89976c85`) красный:
|
||
|
||
```
|
||
FAILED (1):
|
||
- viewCenterRestored: expected true, got false
|
||
```
|
||
|
||
Сценарий смока (не новый, существовал до #437): View zoom 1.6 со смещённым центром → `devices`-редактор zoom 2.5 → назад в View. Ожидание — та же точка центра, что была до входа в редактор (`docs/houseplan-card.ts` явно документирует это как контракт: «the pre-editor viewport (zoom AND center) comes back»).
|
||
|
||
Численно (инструментированный прогон, X/Y координаты логической viewBox):
|
||
- ожидалось `c = [-890.839…, -532.149…]`;
|
||
- получено `c = [-806.25, -532.149…]` — Y точный, X расходится на **84.59** единиц.
|
||
|
||
**Это не регресс, внесённый коммитом `89976c85`.** Проверено прямым запуском того же инструментированного смока на материале r1 (`git worktree add` на SHA `96e07b9a`, `npm run bundle:sync`, `node demo/smoke_zoom_out_debug.mjs`) — идентичный результат, `_debug_dist: 84.58904109589048`, до последнего знака. На `origin/dev` (`ed9ee026`, до фичи #437) тот же смок — **зелёный**. То есть регресс внесён где-то в самом фиче-коммите `96e07b9a` («feat: add configurable summary panel»), пережил ревью r1 (в списке гейтов r1 `smoke_zoom_out.mjs` не значится — не был выбран/запущен) и не тронут дельтой r1→r2.
|
||
|
||
Механизм (прочитан, не отлаживался пошагово по всем ветвям): `houseplan-editor-runtime.ts:1283` вызывает `this.host._bootSoftCancel()` безусловно в начале `_setMode()` — при **каждом** переключении режима, а не только по пользовательскому вводу на плане. `_bootSoftCancel()` (новый механизм #437, `src/boot-soft-layout.ts` + `houseplan-card.ts:6559-6568`) в первые `BOOT_SOFT_MS=1500ms` после раскрытия карточки синхронно замеряет высоту шапки и вызывает `_applyView(this._zoom, cx, cy)` от **текущего** `this._view`. Тест переключает режимы сразу после запуска демо-страницы (внутри окна `_bootSoft`), поэтому этот пересчёт вклинивается до того, как `_setMode` фиксирует `_viewModeSnap`/восстанавливает `targetCenterX/Y` из него, и итоговый X смещается.
|
||
|
||
Это существующий, задокументированный в коде контракт продукта («редактор — рабочий инструмент, не то, что пользователь хочет видеть после»), а не пункт AC1–AC26 из ТЗ #437 буквально — но поломан кодом, который #437 сам добавил (`_bootSoftCancel`, вызываемый из каждого `_setMode`), и относится к разряду «изменение ухудшает смежное поведение» (PROCESS: жёлтый вердикт правомерен и при выполненных AC, если задета соседняя функциональность). В скоупе задачи — правится в этой же задаче, отдельный issue не заводится (#202).
|
||
|
||
Воспроизведение (готовая команда): `node demo/smoke_zoom_out.mjs` на `HEAD` — красный, `viewCenterRestored: expected true, got false`.
|
||
|
||
### Low (не блокируют, решение ревьюера — снято с записью)
|
||
|
||
- **L1** — `_prepareCameraCommand`, `_bootSoftCancel`, `_stagePointerDown` (`houseplan-card.ts`) объединяют по два независимых оператора в одну строку через `;` (например `this._bootSoftCancel(); if (this._modeTransitionBusy) this._cancelModeTransition(true);`). Работает корректно, но снижает читаемость и диффопригодность по сравнению со стилем остального файла. Не требую правки — точечная правка форматирования не стоит отдельного цикла ревью; можно поправить попутно при фиксе M5, раз файл всё равно будет тронут.
|
||
- **L2** — та же строка `import { measuredCardHeaderHeight, settleSoftStageLayout } from './boot-soft-layout';` дописана в конец существующей многострочной секции импорта `viewport-transition` через `;` вместо отдельного `import`-блока (`houseplan-card.ts` diff в области строки 321-324). Работает, но нарушает обычный стиль одного импорта на блок.
|
||
- **L3** — `src/summary-panel-identity.ts`: `structuralPath()` по-прежнему строит идентичность через `children.indexOf(node)` на каждом уровне до ближайшего `hui-card`. Кеш (`WeakMap`, ключ — сам DOM-узел обёртки) закрывает конкретный сценарий из теста-свидетеля (реордер колонок Masonry **в одной сессии** не меняет ключ) — то есть закрывает ровно то, что просил r1. Но ТЗ §8.2 буквально требует для Masonry «индекс в исходном упорядоченном `cards`, не номер визуальной колонки»; реальная разметка `hui-masonry-view` недоступна в demo-стенде (тот же повод, что был у r1 — «разобрано чтением, не воспроизведено исполнением»), поэтому не могу подтвердить или опровергнуть, что **первое** вычисление пути (до попадания в кеш, например после полной перезагрузки страницы) совпадает с логическим индексом карточки, а не с колонкой, в которую её в этот раз распределил алгоритм балансировки высот. Снимаю как Low, а не Medium: конкретный дефект, который просил доказать r1 (нестабильность при живом реордере), закрыт и подтверждён исполнением; это уже архитектурное сомнение за пределами того, что можно воспроизвести в этом стенде.
|
||
|
||
## Что проверено и корректно
|
||
|
||
- M1–M4 из r1 закрыты по существу (см. таблицу выше), с независимым воспроизведением там, где это было возможно исполнением, и явной пометкой «прочтением» для L3.
|
||
- `af441149` (сортировка backend-импорта) и `336d8f30` (правка `smoke_danger_confirm_branches.mjs` под инертный `<style data-hp-summary>` в теневом DOM) — прочитаны, обе точные и по существу: `336d8f30` не маскирует реальный дефект, `<style>`-узел действительно не несёт pointer/decision-поверхности (`src/summary-panel-runtime-loaded.ts:228-235`).
|
||
- Оба changelog правятся в одном коммите с `User-Visible: yes` (`3c1a9f8f`, `89976c85`) — трейлеры соблюдены.
|
||
- `node scripts/process-gate.mjs` — чисто, трейлеры и имя ветки в порядке.
|
||
- Три копии бандла байтово идентичны на `HEAD`.
|
||
|
||
## Чего не проверял
|
||
|
||
- `python -m pytest tests_backend -q` в этом раунде — нет HA-harness в окружении ревью; дельта трогает backend только порядком импорта, риск оценён как нулевой, полный прогон наследуется из r1 (396 passed, 3 skipped на `96e07b9a`).
|
||
- `npm run golden:verify` и `check-docs --screenshots=strict` — предрелизные гейты, не гейт ревью; дельта не меняет видимый рендер панели.
|
||
- `node scripts/model-invariants.mjs` — дельта не трогает геометрию/`layout`/`marker.space`/толщины стен, прогонять не по чему.
|
||
- Полная матрица `demo/smoke_*.mjs` (232 файла) — прогнаны только целевые (M1–M4) и камера-центричные из выборки `smoke-select` по дельте; остальные 40 «слабых» совпадений из выборки не прогонялись (общие символы `_config`/`_mode`, вероятность связи с этой дельтой низкая, полный прогон — предрелизный гейт).
|
||
- Реальная разметка `hui-masonry-view`/`hui-sections-view` — недоступна в demo-стенде ни в r1, ни в r2 (см. L3).
|
||
|
||
## Вердикт
|
||
|
||
Жёлтый. Находки r1 (M1–M4) закрыты и подтверждены. Новая находка этого раунда — **M5**, регресс восстановления камеры View при возврате из редактора: не внесён дельтой r1→r2, но и не был замечен в r1 (смок не входил в выборку гейтов r1) и остаётся красным на материале r2. В скоупе задачи, фикс — в этой же задаче, без отдельного issue.
|
||
|
||
---
|
||
|
||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `issue/437-summary-panel`, коммит `89976c85edc4` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||
- Дерево материала: `d4d69013ad77f79890655837333acc57f552b765`
|
||
```
|
||
git log --all --format='%H %T' | grep d4d69013ad77
|
||
```
|
||
- ТЗ `docs/specs/437-summary-panel.md`, блоб `58a2db80c2161079bc9044c107063e54bfcf0be6`
|
||
```
|
||
git log --all --find-object=58a2db80c2161079bc9044c107063e54bfcf0be6 -- docs/specs/437-summary-panel.md
|
||
```
|