Files
houseplan-card/legacy/reviews/v1.73.0/CODE-REVIEW-437-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

269 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-437-r1
Issue: [#437](https://github.com/Matysh/houseplan-card/issues/437) — конфигурируемая read-only сводная панель поверх плана.
Материал: `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD` на SHA `96e07b9a0255eef3a55cd6c3be08198a0adcc7ba`.
ТЗ: [docs/specs/437-summary-panel.md](../../specs/437-summary-panel.md), принято ревью ТЗ r2 (зелёное, High 0/Medium 0).
Этап: код-ревью, заход r1, лимит циклов 4/4 (полный трек).
## Скоуп диффа
82 файла, +5131/−424. Продукт: `src/summary-panel*.ts` (новые), точечные правки
`houseplan-card.ts`/`houseplan-panel.ts`/`config-store.ts`/`types.ts`/`chrome.styles.ts`;
backend — `validation.py`/`websocket_api.py`/`const.py` (новый namespace
`settings.summary_panel`, версионирование, change-aware проверка ссылок).
Тесты: `test/summary-panel.test.mjs`, `tests_backend/test_summary_panel.py`,
`demo/smoke_summary_panel.mjs`, правка `test_support_package.py`,
`test/styles-split.test.mjs`. Документация: оба changelog, USER-GUIDE ru/en,
UX-MODES, TOUCH-SUPPORT, CONFIG-COMPATIBILITY, ARCHITECTURE,
`scripts/config-field-registry.mjs`/`config-schema.json`. Бандл пересобран и
синхронизирован в трёх копиях (класс D).
Отдельно фиксирую для трассируемости: в теле issue и в ТЗ несколько раз
записана «команда владельца: ТЗ → S5-ready, затем остановка, реализацию не
начинать» (комментарии `5586184163`, `5586343543`, `5586611633`). Реализация
всё же началась двумя минутами позже тем же account/committer'ом
(`5586638954` → коммиты `Sergey Matyunin <s.matyunin@justbusiness.site>`).
Поскольку это тот же git-identity, что и автор ТЗ и постановщик самой
команды на остановку, трактую это как собственное решение владельца
продолжить, а не как обход чужого запрета агентом; отдельного авторизующего
комментария от третьей стороны не требуется по роли. Это наблюдение, не
находка код-ревью — фиксирую, чтобы SHA-цепочка была прослеживаема.
## Как проверялось — гейты
Зелёного Validate на `96e07b9a` не найдено — прогнал сам.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | pass, 0 ошибок |
| Юниты | `npm test` | 2264 passed, 0 failed, 1 skipped (33.7s) |
| Сборка + бандл | `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 origin/dev --head HEAD` | pass, 1907 новых строк в 14 файлах, новых `any` нет |
| Докскрины | `node scripts/check-docs.mjs` | RED — отпечаток скриншотов устарел. Ожидаемо и не блокирует ревью (PROCESS §8: это предупреждение на обычном push в `dev`, жёсткий гейт только у релиз-кандидата с `Release:`); пересъёмка остаётся обязанностью перед бетой |
| Бюджет бандла | `node scripts/bundle-budget.mjs` | pass; initial View 291234 B / потолок 292000 B, запас 9832 B — ниже порога 15000 Б, скрипт сам печатает предупреждение и ссылку на трекнутый долг #367. Не новая находка этой задачи — существующий механизм уже сигналит владельцу, отдельно не завожу |
| Смок-подбор | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 73 «прямых совпадения» из 232 — почти все по широким символам самого `houseplan-card.ts` (`_mode`, `_config`, `_model`, `stopPropagation`), сигнал зашумлён. Прогнал точечно: новый `demo/smoke_summary_panel.mjs` (ниже) и `demo/smoke_kiosk.mjs`, единственный, где диф трогает конкретно изменённую функцию `_saveKioskScale` не по совпадению общего имени, а по факту рефакторинга её тела |
| Новый браузерный смок | `node demo/smoke_summary_panel.mjs` | OK (перепрогнал сам, не только со слов автора) |
| Существующий смок (флагован выше) | `node demo/smoke_kiosk.mjs` | **FAIL** — необработанное исключение, см. находку M1 |
| Backend, чистый набор | `python -m pytest tests_backend/test_summary_panel.py -q` (voluptuous/pytest доустановлены локально) | 9 passed |
| Backend, полный HA-harness | — | не прогонял: `.venv-backend` в этом окружении отсутствует (не облачный агент); доверяю отчёту автора (`.venv\Scripts\python.exe -m pytest tests_backend -q` → 396 passed, 3 skipped) с оговоркой — не перепроверено независимо |
| Инварианты геометрии | `node scripts/model-invariants.mjs` | не прогонял: диф не пишет ни рёбра/толщину/`layout`/`marker.space`/`open_spans` — `totalCleanFloorAreaM2` только читает существующую геометрию для суммы площади, ничего не изменяет в модели |
| golden | `npm run golden:verify` | не прогонял: overlay — DOM-sibling вне SVG/zoomwrap слоя (подтверждено чтением `summary-panel-style.ts`/`renderPanel`), существующий рендер плана не тронут; полный набор — предрелизный гейт |
| performance | — | не прогонял: не названо блокирующим в AC для этого раунда, откладываю на пре-релиз по PROCESS §8; логику таймера/мемоизации проверил чтением (см. ниже) |
## Находки
Все — Medium, в скоупе задачи, чинятся в этой же итерации (без High это жёлтый
вердикт, PROCESS §2.7). Ни одна не выходит за рамки #437, отдельных issue не
завожу.
### M1 — существующий смок `demo/smoke_kiosk.mjs` падает необработанным исключением на этом SHA
**Файл:** `demo/smoke_kiosk.mjs:52` (`out.persisted = JSON.parse(localStorage.getItem('houseplan_card_kiosk_v1')).icon === 1;`).
Диф меняет `_saveKioskScale` (`src/houseplan-card.ts`) так, что она целиком
делегирует в `LoadedSummaryPanelRuntime.saveScale` → `saveLocal`, которая (по
ТЗ §8.3, сознательно) пишет только в новый per-instance ключ
`houseplan.summary-panel.v1:...` и **больше не пишет** в легаси-ключ
`houseplan_card_kiosk_v1` («старый ключ не удалять и не переписывать» — это
верно и явно требуется ТЗ). Продуктовое поведение корректно: я независимо
проверил, что масштаб иконок и диалог размеров реально работают
(см. «Проверено» ниже). Проблема в том, что старый смок, всё ещё
утверждающий «после сохранения легаси-ключ обновился», не был обновлён вместе
с рефакторингом, и падает `TypeError: Cannot read properties of null (reading
'icon')`, потому что `localStorage.getItem('houseplan_card_kiosk_v1')`
теперь `null` (ключ никогда не создавался в demo-профиле — легаси-seed
срабатывает только если ключ уже существовал).
**Чем краснеет:** воспроизведено напрямую — `node demo/smoke_kiosk.mjs`
на `96e07b9a` падает необработанным исключением (см. таблицу гейтов),
поэтому ни одна из последующих 4 проверок в этом файле (диалог размеров,
карусель, пауза после касания, обычная карточка) в принципе не выполняется —
файл прерывается на середине. `scripts/smoke-select.mjs` называл этот файл
прямым совпадением по символу `_saveKioskScale`; PROCESS §8 требует именно
такие смоки гонять до code-review — это не сделано.
**Правка:** обновить assertion на новый контракт (например, прочитать
per-instance ключ через `summaryLocalKey`, либо проверить, что легаси-ключ
остался нетронутым, если он не существовал изначально) так, чтобы файл не
падал и продолжал проверять оставшиеся пункты списка.
### M2 — исключение «удалённого устройства» в подсчёте Q2 не имеет свидетеля
**Файл:** `src/summary-panel-metrics.ts:25` и `:45` (`representedHaDeviceIds`).
ТЗ §7.2/§12 явно требует численную фикстуру «removed binding исключён,
explicit restored child возвращает ровно 1», и AC18 заявляет это как защиту.
Я снял обе проверки `removed.devices.has(...)` по очереди (через-area путь и
через-marker путь) и перезапустил `test/summary-panel.test.mjs` —
оба раза все 12 тестов остались зелёными: фикстура в тесте
(`#437 device total counts unique represented real HA device ids…`) не
содержит ни одного маркера с `removed: true`, поэтому обе ветки исключения
никогда не выполняются под тестом.
**Чем краснеет:** мутация выполнена дважды, результат приведён (см. таблицу
гейтов и вставки выше): `git diff` мутации доступен в истории этой сессии,
после проверки файл возвращён в исходное состояние (`git status` чист).
**Правка:** добавить в фикстуру маркер с `removed: true` (или эквивалент из
`removedPlanBindings`) и явно восстановленную дочернюю entity, как в
числовом примере ТЗ §12, чтобы тест ловил регрессию в любой из двух ветвей.
### M3 — tap-target реордер/удаления в форме настроек меньше заявленных 44×44 CSS px
**Файл:** `src/summary-panel-style.ts:197-207` (`.summary-editor-row button { min-width: 34px; height: 34px; }`).
ТЗ §5.4 явно объявляет: «Touch editor: supported только для новой простой
формы сводки» — то есть именно эта форма (а не только сам overlay и
составной контрол в шапке) получает touch-контракт — и тут же: «Размер
tappable зоны **каждого** контрола ≥44×44 CSS px». Кнопки ↑/↓/удалить у
блоков и строк в `summary-panel-editor.ts` используют этот класс и физически
дают 34×34 px — на 23% меньше контракта. Добавленный в этом же диффе
`docs/TOUCH-SUPPORT.md` (строки 48-51) сам сузил обещание до «панель и обе
половины составного контрола» + «диалог сохраняет доступность контента» —
то есть документация уже разошлась с числовым контрактом принятого ТЗ,
не будучи для этого отдельно согласована с владельцем (§7.1 разрешает менять
только продуктовые решения владельца, не через документацию по умолчанию).
**Чем краснеет:** проверено чтением CSS и вычислением фактического размера
(34 vs заявленных 44); отдельного автотеста на размер контролов в диффе нет
(не искал, чтобы не плодить ложный вывод — `grep -n "44"` по тестам/смокам
не нашёл проверок размера ни для одного контрола формы).
**Правка:** увеличить `.summary-editor-row button` (и любые другие
интерактивные элементы формы у которых итоговый tap-target <44px) минимум до
44×44 либо явно обновить и ТЗ, и `TOUCH-SUPPORT.md` с owner-видимой правкой
контракта, если 34px — осознанное сужение только для мыши/клавиатуры (тогда
формулировка ТЗ «каждого контрола» вводит в заблуждение и тоже требует
правки).
### M4 — идентичность карточки (AC21) не имеет ни выделенного резолвера, ни свидетеля теста
**Файл:** `src/summary-panel-runtime-loaded.ts:236-250` (`placementSlot`).
ТЗ §8.2 требует: «Изолированный resolver связывает enclosing `hui-card` с
логическим индексом в native view: Sections — `[viewIndex, sectionIndex,
cardIndex]`, Masonry — индекс в исходном упорядоченном `cards`, **не номер
визуальной колонки**». В коде единственный механизм — `placementSlot()`,
общий для всех случаев: подъём по `parentNode`/shadow-host с записью
`localName:childIndex` на каждом уровне. Для Masonry-вида HA перераспределяет
карточки по колонкам по высоте при изменении ширины окна — то есть DOM-индекс
карточки в родителе именно то, что ТЗ называет «номером визуальной колонки» и
явно запрещает использовать. AC21 обещает доказательство «Native
Sections/Masonry/nested fixtures, reload/remount/reflow» — этого нет ни в
`test/summary-panel.test.mjs` (нет упоминаний `placementSlot`/`preferenceKey`/
identity), ни в `demo/smoke_summary_panel.mjs` (по описанию хендоффа: «right/
bottom, small-card hide/restore, local intent, overlay geometry, read-only
surface, lazy full form» — без identity/masonry/reflow).
Признаю: §14 ТЗ помечает механику «per-card key по logical host path»
техническим решением, свободным для правки ревьюером — значит сам факт
отказа от буквального `[viewIndex, sectionIndex, cardIndex]` не обязан быть
находкой сам по себе. Находка именно в том, что **защитный AC без названного
свидетеля** (PROCESS §2.7) — нет ни одного теста, который доказывал бы, что
выбранная общая реализация действительно переживает Masonry-реflow, при том
что это ровно тот сценарий, который ТЗ явно называет риском.
**Чем краснеет:** не воспроизведено исполнением (реальный `hui-masonry-view`
недоступен в demo-стенде — честно пишу «проверено чтением, не
исполнением»); риск обоснован конкретным механизмом браузерного
column-балансирования Masonry-вида, который переставляет карточки между
колонками при изменении ширины без изменения состава `cards` в конфиге.
**Правка:** либо добавить smoke/unit фикстуру, эмулирующую изменение
DOM-порядка карточки без изменения logical config (переставить `children` у
фейкового контейнера и убедиться, что `preferenceKey()`/`placementSlot()` не
меняется), либо прочитать логический индекс из `hui-view`/`lovelace` config,
если он доступен, вместо позиции в DOM.
## Проверено и корректно
- **Модель/валидация** (`summary-panel.ts`): `cpLength` считает Unicode
code points тем же способом, что backend `len(str)` (Python 3 строки —
всегда code points) — лимиты 48/64 согласованы на обоих концах, подтверждено
тестами по обе стороны и мутацией backend-регекса (см. ниже).
- **Backend reference validation** (`validate_summary_panel_references`):
мутация (убрал проверку `entity_id not in readable_entity_ids`) уронила
`test_reference_validation_allows_old_broken_ids_but_rejects_new_ones` —
тест умеет падать, защита реальна.
- **Совместимость** (`preserve_summary_panel_namespace`,
`_future_summary_panel`): старый клиент не стирает namespace, будущая
неизвестная версия остаётся lossless — оба пути покрыты
`test_old_client_omission_preserves_namespace_and_future_version_losslessly`.
- **Приватность** (`test_support_package.py`): добавленный `summary_panel`
с приватными строками явно исключён из `plan_backup` (`assert
"summary_panel" not in package[...]`), тест прогнан и зелёный.
- **Ленивая загрузка**: `test/summary-panel.test.mjs` проверяет, что
`summary-panel-editor`/`-runtime-loaded` не попадают в `initialViewFiles`
манифеста и что карточка не импортирует редактор статически — подтверждено
чтением манифеста и самим прогоном.
- **Отсутствие сервисных вызовов / только текст**: регэксп-тест на
`callService(` в загруженном рантайме и редакторе зелёный; весь
пользовательский текст идёт через `lit-html` интерполяцию (авто-экранирование),
`unsafeHTML` нигде не импортирован — проверено чтением обоих файлов.
Совпадает с secure-device инвариантом SCOPE.md (замки эта задача не трогает).
- **aria-pressed** отражает `local.show` (сохранённое намерение), а не
временную видимость от mobile/fits — проверено чтением `renderControls`,
соответствует явному требованию §4.1 ТЗ.
- **Layout-математика** (`resolveSummaryLayout`): юнит-тест воспроизводит
ровно граничные примеры из ТЗ §5.2 (303/304, 185/186, 245/246 с
`controlTop:72`) — совпадает побитово.
- **Разделение shared/local Save**: `saveDialog` не трогает `localShow` при
ошибке записи и не откатывает уже применённый local при отказе сервера —
прочитано, соответствует §9.1 контракту «атомарно значит без частичного
draft», конфликт (`_cfgRev !== baseRevision`) корректно триггерит `conflict`.
Не проверено исполнением two-client сценария (см. ниже).
- **Таймер часов**: единственный `setTimeout` на видимую панель с датой,
снимается на `hidden`/disconnect, пересчитывает контекст (locale/tz) —
прочитано, логика соответствует §7.4 («не создавать timer на строку»).
- **device_count/area кэш**: мемоизация по `cfgEpoch`/`layoutRev`/
`registryRev` — обычное обновление состояния сущности не должно
перезапускать union/roster; прочитано, инвалидация ключей выглядит верной,
не проверено performance-профилем (см. «не проверял»).
## Чего не проверял и почему
- **Полный HA backend harness** (`test_ha_*.py`) — окружение без
`.venv-backend`; доверяю отчёту автора без независимой перепроверки.
- **Golden/скриншоты** — предрелизный гейт по PROCESS §8, diff не меняет
существующий рендер плана (overlay вне SVG-слоя, подтверждено чтением).
- **Полный набор `demo/smoke_*.mjs`** — не оправдано объёмом диффа: прогнал
выборочно новый смок и единственный существующий смок, реально задетый
изменённым кодом (`smoke_kiosk.mjs`, см. M1); остальные 230 не выбраны
диффом по существу (шум smoke-select — общие имена полей).
- **Performance-профили** (AC25/§12) — не названы блокирующими для этого
раунда; логика памятки/таймеров проверена чтением, не измерением.
- **Реальное Masonry/Sections-переразмещение HA** — недоступно в
demo-стенде; см. M4, отмечено как «проверено чтением».
- **Многоклиентная конкурентная запись (AC19)** — проверено чтением кода
optimistic-locking пути (`expected_rev`, `_cfgRev`), не воспроизведено
реальным two-client прогоном.
- **Реальные touch-устройства** — размер tap-target оценён по CSS-значениям
(см. M3), не по замеру на физическом экране.
## Итог
High: 0 · Medium: 4 (все в скоупе, чинятся в этой же задаче) · Low: 0.
Три из четырёх находок (M1, M2, M3) — конкретные, воспроизведённые
инструментально в этой сессии (упавший смок, две мутации без реакции теста,
измеренный CSS-размер). M4 — разобрана чтением с честной пометкой, куда
дотянуться исполнением не удалось.
Вердикт: жёлтый. Возврат автору на исправление M1–M4 в рамках текущей
задачи, без нового issue.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/437-summary-panel`, коммит `96e07b9a0255` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `ae3f49aa4859ba86376dd881a1dea9e7d1e08ba2`
```
git log --all --format='%H %T' | grep ae3f49aa4859
```
- ТЗ `docs/specs/437-summary-panel.md`, блоб `58a2db80c2161079bc9044c107063e54bfcf0be6`
```
git log --all --find-object=58a2db80c2161079bc9044c107063e54bfcf0be6 -- docs/specs/437-summary-panel.md
```