Files
houseplan-card/legacy/reviews/v1.64.0/CODE-REVIEW-146-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

301 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 — issue #146, cycle r1
Вердикт: **красный** · цикл r1/4 · High: 1 · Medium: 0
Ветка: `issue/146-four-phase-sun-background` · implementation-коммит
[`debb13b`](https://github.com/Matysh/houseplan-card/commit/debb13baa280042c79a004c97152e3b1be5ab11b)
· ТЗ: [`docs/specs/146-four-phase-sun-background.md`](../../../docs/specs/146-four-phase-sun-background.md)
(зелёный [`SPEC-REVIEW-146-r1`](SPEC-REVIEW-146-r1.md), High:0, Medium:1 → #147,
не блокирует).
## Скоуп проверки
`git diff origin/dev...HEAD` — 35 файлов, 2575(+)/782(-). Ядро изменений:
`src/sun.ts` (новый `resolveDayCycle`/палитра/резолвер), `src/day-cycle-render.ts`
(новый файл — общие layer-шаблоны), `src/houseplan-card.ts` (lifecycle,
defaults, рендер стейджа), `src/space-card.ts`/`src/space-render.ts`
(статическая карточка), `src/styles.ts` (весь визуальный контракт),
`custom_components/houseplan/{store,const,import_export}.py` (миграция и
export/import), `src/i18n/{en,ru}.json`, тесты (`test/sun.test.mjs`,
`tests_backend/test_ha_import_export.py`, smoke), документация (`SUN.md`,
`ARCHITECTURE.md`, `CONFIG-COMPATIBILITY.md`, `USER-GUIDE.ru.md`,
`TESTING.md`, `STYLING-HOOKS.md`) и оба changelog.
Единственный implementation-коммит `debb13b` несёт `Issue: #146` ·
`User-Visible: yes`; оба changelog обновлены в этом же коммите — требование
выполнено. Три копии bandle идентичны (см. ниже).
## Как проверялось
Дешёвые гейты (всегда):
- `npx tsc --noEmit` → **зелёный**, без вывода.
- `npm test` → **802/802 green**.
- `npm run build` → зелёный; свежая пересборка дала SHA-256
`67293527e2154b2bc879d45a173dfba64c5a70cd090e770059e2b08c7c6ea0fe` для всех
трёх файлов (`dist/houseplan-card.js`,
`custom_components/houseplan/frontend/houseplan-card.js`,
`demo/srv/assets/houseplan-card.js`) — совпадает с хендофф-комментарием
автора; `git status` после билда пуст (закоммиченные копии побайтно
совпадают со свежей сборкой).
Гейты по необходимости (diff меняет рендер/CSS-layering/lifecycle на всех
поверхностях фона — обоснование запуска целевых smoke и golden):
- `node demo/smoke_bg_color.mjs` → все проверки `true`, `OK` (paper opacity,
night-does-not-dim-plan, static/kiosk/space-card parity).
- `node demo/smoke_sun.mjs` → все проверки `true`, `OK` (существующие оконные
лучи/compass не задеты).
- `node demo/smoke_sun_live_bg.mjs` → все проверки `true`, `OK`.
- `node demo/smoke_general_settings.mjs` → все проверки `true`, `OK`, включая
`newSpaceUsesDaynight`/`floorImportUsesDaynight` (AC10).
- `node demo/smoke_render_perf.mjs` → все проверки `true`, `OK`, включая
`clockTickModelBuilds: 0`/`modelBuildsPer10Renders: 0` — прямое
доказательство AC14 (нет geometry/device rebuild на clock tick).
- `npm run golden:verify` (полный набор, инструмент запрещает частичный прогон
в режиме verify) → **нашёл найденный ниже High**. Дополнительно: 4 новые
`day-cycle-*` сцены и другие тематические сцены (`plan-snap-*`,
`wall-junctions-*`, `isometric-wall-junctions-dark`) — `missing-baseline`;
для контроля я прогнал тот же `golden:verify` на чистом `origin/dev` (тот же
Linux/Chromium в этой среде, `demo/golden/baselines/**` не менялся этим
diff'ом) — там `plan-snap-*`/`wall-junctions-*` тоже отсутствуют, а
`geometry-plan-editor-dark`/`tray-wide-selection-en`/`tray-wide-tool-ru`/
`tray-medium-group-en` тоже `different`. Это подтверждает: это все —
пред-существующая среда-специфичная нестабильность/техдолг, не относящийся к
#146. Но `large-house-zoom-250-dark` на `origin/dev` **passed**, а на этой
ветке (дважды, воспроизводимо) — `different`. Разбор ниже, в «Находках».
- `python -m pytest tests_backend` — не прогонялся: в этой среде отсутствует
пакет `homeassistant` (`ModuleNotFoundError`), значит `test_ha_*.py`
(включая новый `test_ha_import_export.py`) молча пропускаются, а зелёный
результат ничего не доказывает (`AGENTS.md`, «Backend»). Backend-логика
(`store.py::migrate_config_background_mode`, `import_export.py`) и сам новый
тест проверены **чтением, не исполнением** — см. «Что проверено».
- Performance-профили/Full Performance — не прогонялись: предрелизный гейт
(AC16), а таргетированный `smoke_render_perf.mjs` уже дал прямое числовое
доказательство отсутствия rebuild на clock tick (см. выше).
## Находки
### [High] `.zoomwrap`'s новый `z-index: 1` прячет `.zoombadge` за планом на любом zoom>100% — независимо от `bg_mode`
**Файл:** `src/styles.ts:181-185` (`.zoomwrap { position: absolute; inset: 0;
z-index: 1; }` — строка `z-index: 1` добавлена этим diff'ом) против
`src/styles.ts:472-485` (`.zoombadge` — `position: absolute`, без `z-index`)
и `src/houseplan-card.ts:14758-14760` (`${this._zoom > 1 ? html`<div
class="zoombadge">...` : nothing}`, рендерится как **сиблинг** `.zoomwrap`,
после него в DOM).
**Причина.** До этого diff'а ни `.zoomwrap`, ни `.zoombadge` не имели
`z-index` — оба в слое `auto`, порядок рисования определялся порядком в DOM, и
`.zoombadge` (декларирован ПОСЛЕ `.zoomwrap`) закономерно рисовался сверху.
Этот diff добавил `.zoomwrap { z-index: 1 }`, чтобы держать план выше нового
`.hp-day-cycle-env { z-index: 0 }` (`styles.ts:201-208`). Но
`.hp-day-cycle-env` и без этого правила уже стоял бы ниже `.zoomwrap` по чистому
порядку DOM (он декларирован раньше) — явный `z-index: 1` был не нужен для
заявленной цели и стал побочным регрессом: положительный `z-index` выводит
ВСЁ содержимое `.zoomwrap` (план, комнаты, устройства, measure-layer) в более
высокий слой стекинга, чем любой сиблинг с `z-index: auto`, **независимо от
порядка в DOM**. `.farhint`/`.homearrow`/`.recoveryoverlay`/`.bootveil` не
задеты — у них уже был явный `z-index` (12/12/75/40). У `.zoombadge` его не
было и не появилось.
**Сценарий отказа:** любой пользователь View/kiosk, увеличивающий план сверх
100% (масштабирование колёсиком/пинчем — обычное действие, никак не связанное
с `bg_mode`), теряет видимый индикатор процента зума в левом нижнем углу —
элемент существует в DOM (`pointer-events: none`, не блокирует
взаимодействие), но визуально полностью скрыт под планом.
**Воспроизведено, не только прочитано:**
1. `npm run golden:verify` на этой ветке — сцена `large-house-zoom-250-dark`
(View, `zoom: 2.5`, фикстура `bg_mode` не задан → эффективный режим
`static`, то есть день/night-фон здесь вообще не активен — регресс не
специфичен для `daynight`) даёт `different`; повторный прогон —
тот же результат (детерминированно, не флейк).
2. Тот же `golden:verify` на чистом `origin/dev` (тот же чекаут, тот же
Chromium) — та же сцена **passed**.
3. Визуальное сравнение `demo/golden/baselines/large-house-zoom-250-dark.png`
(бейзлайн) и `artifacts/golden/actual/large-house-zoom-250-dark.png`
(текущий рендер): единственное отличие — плашка `250%` в левом нижнем углу
присутствует на бейзлайне и полностью отсутствует на актуальном рендере;
`artifacts/golden/diff/large-house-zoom-250-dark.png` подсвечивает magenta
ровно область этой плашки, весь остальной кадр — «без изменений» (серый с
низкой альфой).
**Не покрыто ни одним AC/smoke этой задачи** — ни один из существующих
`demo/smoke_*.mjs` не проверяет видимость `.zoombadge` при zoom>1, поэтому
`npm test`/targeted smoke зелёные, а регресс виден только через golden-пиксели
существующей (не новой) сцены `large-house-zoom-250-dark`.
**Требуется:** дать `.zoombadge` собственный `z-index` выше `1` (например,
как у `.farhint`/`.homearrow` — `12`), либо убрать не обязательный для цели
`z-index: 1` с `.zoomwrap` (естественный порядок DOM уже держит его выше
`.hp-day-cycle-env` без этого правила) — на выбор автора; и повторный прогон
`npm run golden:verify` (весь набор — гейт verify не даёт частичного) до
`passed` на `large-house-zoom-250-dark`, а также визуальный просмотр 4 новых
`day-cycle-*` сцен перед их принятием как baseline.
## Что проверено и корректно
- **AC1 (real-sun резолвер границ)** — `test/sun.test.mjs` («day cycle: real
sun selects exact four-phase boundaries») бьёт по обеим сторонам ±6° и
точным границам при `rising`/`falling`; тест умеет падать (замена `<=`/`>=`
на `<`/`>` в `dayCyclePhaseFromSun` немедленно ломает утверждения на
границах).
- **AC2 (atomic clock-fallback на garbage)** — `test/sun.test.mjs` («strict sun
snapshot keeps rays compatibility separate») перебирает
missing/NaN/Infinity/non-boolean для каждого поля и подтверждает `null`
(→ fallback), а `sunStateOf` (оконные лучи) не получил новое требование
`rising` — соответствует ТЗ §7.1. `smoke_sun_live_bg.mjs`:
`invalidSampleUsesClock: true`. Известное расширение до «любого из
elevation/azimuth/rising» (а не только elevation/rising) реализовано именно
так, как задокументировано в Medium-1 зелёного SPEC-REVIEW (→ #147,
сознательно не блокирует, не новая находка).
- **AC3 (позиция света следует реальным данным, fallback — дуга прототипа,
geometry не пересчитывается)** — `test/sun.test.mjs` («real decorative
position…», «fallback arc and night visibility…») численно совпадает с §8.1/
§8.2 ТЗ (east/south/west/north, дуга 05:00→13:00→21:00); `night.sunOpacity
=== 0` подтверждён и в unit, и в `smoke_sun_live_bg.mjs`
(`nightLightHidden: true`). Отсутствие geometry rebuild — прямое числовое
доказательство `smoke_render_perf.mjs` (см. «Как проверялось»).
- **AC5 (пиксели плана не меняются между фазами)** — прочитан
`src/styles.ts:198-248`: единственный phase-зависимый эффект на SVG —
`filter: drop-shadow(...)` на групповом `.hp-paperg` (три zero-offset
drop-shadow, как требует §9) — drop-shadow не подмешивает цвет в пиксели
самой фигуры, только рисует внешний halo за её alpha-контуром, поэтому
внутренность footprint остаётся байт-в-байт прежней. Подтверждено визуально:
`artifacts/golden/actual/day-cycle-dawn-dark.png` и `...night-dark.png` —
комнаты/стены/labels идентичны по цвету между сценами, виден только
внешний halo и фон вокруг плана. `smoke_bg_color.mjs`:
`nightDoesNotDimPlan/nightPaperStaysOpaque/paperOpaque: true`. Старый
потребитель `dayPhase().planDim` удалён из `_targetBrightness`
(`houseplan-card.ts:908-911`, теперь безусловно `return 1`) и из
`_stageBg`/`transitionBrightness` — риск №1 таблицы §19 ТЗ закрыт.
- **AC6 (переход 1100ms, plan не мигает, reduced-motion мгновенно)** —
`smoke_sun_live_bg.mjs`: `phaseTransitionIs1100ms: true`; в `styles.ts`
четыре константных `.hp-day-cycle-bg` слоя честно кросс-фейдят через `opacity
1100ms` (не декларация `transition` на самом градиенте) — соответствует
требованию §11.1 «действительно интерполируемые CSS primitives»; `@media
(prefers-reduced-motion: reduce)` отключает все четыре анимируемых правила
(`transition: none`).
- **AC7 (одна фаза на full View/kiosk/static card, editors не задеты)** —
`smoke_bg_color.mjs`: `kioskApplies/kioskSharesDay/staticCardApplies/
staticCardSharesDawn: true`; `_dayCycleState()` возвращает `null` при
`viewWeight <= 0` (editors) — прочитано в `houseplan-card.ts:12654-12659`.
- **AC8 (фон работает без north/sun.sun, оконные лучи — нет)** —
`smoke_sun_live_bg.mjs`: `backgroundIndependentOfNorth/
raysStillNeedNorth: true`. i18n-хинты (`gs.sun_missing`, `gs.north_hint`)
корректно переписаны на «фон работает, лучи — нет» в обеих локалях.
- **AC9 (однократная идемпотентная миграция)** — прочитан
`custom_components/houseplan/store.py:29-71`: `migrate_config_background_mode`
— чистая функция, no-op при уже валидном `bg_mode`, no-op на документах без
`config` (layout/virtual-light stores), сохраняет неизвестные поля
(`copy.deepcopy` + точечная модификация); вызывается только при
`old_minor_version < 2` (после миграции `STORAGE_MINOR_VERSION=2` не
перезапускает её). Новый тест
`tests_backend/test_ha_import_export.py::test_background_defaults_and_store_migration_preserve_legacy_view`
бьёт ровно эти случаи (idempotence через `is migrated`, сохранение
`future_*`/`rev`, no-op для layout-подобного документа) — **проверено
чтением, не исполнением** (см. «Как проверялось», нет `homeassistant`).
- **AC10 (новые defaults материализуют `daynight`, Edit не трогает
существующий mode)** — `smoke_general_settings.mjs`:
`newSpaceUsesDaynight/floorImportUsesDaynight: true`.
`custom_components/houseplan/const.py`: `DEFAULT_CONFIG["settings"]["bg_mode"]
== "daynight"`. Прочитан `houseplan-card.ts:12168` (space-dialog init
сохраняет `null`, если явного override нет) и `:12961`
(`bgModeOf(this._settings, {})` — глобальный dialog открывается с текущим
ЭФФЕКТИВНЫМ режимом, не константой) — открытие+сохранение существующего
space/global dialog без изменения выбора не меняет сохранённый режим.
- **AC11 (export/import контракт §12.4)** — прочитан
`custom_components/houseplan/import_export.py`: `_materialize_global_
background`/`_materialize_space_background` — раздельные fallback
(`create_export` для space передаёт **эффективный global** экспортируемого
инстанса, а `parse_document`/`build_space_merge` для одиночного
space-импорта — жёсткий `"static"`, не режим целевой установки) — в точности
различие, которое требует §12.4 («legacy per-space import не наследует
target»). Новый тест `test_background_mode_is_materialized_across_export_
and_legacy_import` бьёт full/space export, legacy full/space import (через
`parse_document`) и `build_space_merge` — **проверено чтением, не
исполнением**.
- **AC12 (i18n без нового режима, pointer-inert, не двигает focus/hit
targets)** — `src/i18n/{en,ru}.json` обновлены симметрично, третий
публичный токен не добавлен (селектор остаётся `static`/`daynight`,
подтверждено чтением `houseplan-card.ts:14025-14027`). `.hp-day-cycle-env`
— `pointer-events: none` (styles.ts:206) и `aria-hidden="true"`
(`day-cycle-render.ts`). `npm test` включает общий тест полноты i18n-ключей
(802/802 green) — контентная проверка конкретных строк выполнена чтением.
- **AC13 (lifecycle: 30s только в fallback+visible, catch-up, cleanup)** —
`smoke_sun_live_bg.mjs`: `clockTimerArmed/hiddenStopsClock/
visibleRestartsClock/visibilityKeepsHover: true`. Прочитаны
`_dayCycleTick`/`_syncDayCycleClock`/`_dayCycleVisibility`
(`houseplan-card.ts:12660-12706`) и их зеркало в `space-card.ts:143-190`:
таймер существует только при `source === 'clock'` и видимой вкладке,
`disconnectedCallback` чистит и таймер, и `_dayCycleClockKey` в обоих
файлах.
- **AC14 (bounded update, без geometry/device rebuild, RAF/canvas/сети/
storage)** — прямое числовое доказательство `smoke_render_perf.mjs`:
`modelBuildsPer10Renders: 0`, `clockTickModelBuilds: 0`,
`clockTickKeepsModel/Devices/WallUnion/SunRays: true`. Diff не добавляет ни
`requestAnimationFrame`, ни canvas/WebGL, ни новых сетевых/HA/storage вызовов
— подтверждено чтением всего diff (`git show debb13b`).
- **AC15 (гейты, документация, три bundle, один коммит)** — единственный
коммит `debb13b` несёт оба трейлера, оба changelog,
`SUN.md`/`ARCHITECTURE.md`/`CONFIG-COMPATIBILITY.md`/`USER-GUIDE.ru.md`/
`TESTING.md`/`STYLING-HOOKS.md` и все три копии bundle; локальная пересборка
побайтно совпала (см. «Как проверялось»).
- **Инвариант неизменного плана (§10 ТЗ)** — ни один цветовой filter
(`brightness`/`contrast`/`saturate`/`sepia`/`hue-rotate`), overlay или смена
fill не применяется к дереву плана; единственный phase-зависимый filter —
внешний drop-shadow на группе paper (см. AC5 выше). Старые
`_skyPlan`/`_skyElev`/`_skySnap`/`.stage.daynight`/`.skysnap` полностью
удалены, а не оставлены мёртвым кодом рядом с новым путём.
- **Security/network verdict (AC16, §18 ТЗ)** — прочитан весь diff: ни новых
HTML/URL-приёмников, ни новых сетевых вызовов, ни новых HA service calls,
ни новых storage-полей сверх заявленной `bg_mode`-семантики. Негативный
вердикт подтверждается.
## Чего не проверял
- **`npm run golden:accept -- --reviewed`** — не выполнялся и не должен
выполняться на этом этапе: 4 новых `day-cycle-*` сцены (и ряд не связанных с
#146 сцен) остаются `missing-baseline`; принятие baseline — предрелизный шаг
по полному Linux CI артефакту (`AGENTS.md`, `PROCESS.md` §8/13), не гейт
код-ревью. Сами 4 сцены просмотрены визуально (`artifacts/golden/actual/
day-cycle-{dawn,day,dusk,night}-dark.png`) — план не тонирован, ночью не
виден световой круг, halo не создаёт по-комнатных швов; годны как кандидат
на baseline после фикса High и повторного прогона.
- **`python -m pytest tests_backend`** — не выполнялся (нет `homeassistant` в
этой среде); backend-логика и новый тест разобраны чтением (см. «Как
проверялось» и AC9/AC11 выше).
- **Полный browser smoke-suite (127 файлов)** — не прогонялся; прогнаны 5
целевых smoke, относящихся к тронутым поверхностям (`bg_color`, `sun`,
`sun_live_bg`, `general_settings`, `render_perf`), плюс golden как более
широкая пиксельная сеть — она и нашла регресс, не покрытый ни одним из этих
smoke по имени.
- **Performance smoke / Full Performance (эталонные бюджеты)** — не
прогонялись, предрелизный гейт; косвенное доказательство отсутствия rebuild
уже получено через `smoke_render_perf.mjs`.
- **`prefers-reduced-motion`/`forced-colors` в реальном браузере с
ОС-настройкой** — не проверялось интерактивно; поведение разобрано чтением
CSS (`@media (prefers-reduced-motion: reduce)` блок в `styles.ts`) и
соответствует контракту §13 ТЗ.
- **Ручной интерактивный проход в браузере (не headless)** — не выполнялся;
весь визуальный вывод основан на golden/smoke харнессе и прямом просмотре
сохранённых PNG.
## Итог
High: 1 (описан выше, блокирует — регресс видимости `.zoombadge` на
zoom>100%, воспроизведён детерминированно и локализован до одной CSS-строки).
Medium: 0 (единственная содержательная находка этого масштаба — расширение
azimuth-гейта на фазу — уже заведена на этапе ревью ТЗ как
[#147](https://github.com/Matysh/houseplan-card/issues/147) и не повторяется
здесь). Low: 0 новых; ранее отмеченные в SPEC-REVIEW Low-2/Low-3 не относятся к
коду.
Вердикт красный: цикл возвращается автору на исправление стекинга
`.zoombadge`/`.zoomwrap` (см. «Требуется» в находке), с последующим зелёным
`npm run golden:verify` на `large-house-zoom-250-dark` и визуальным
подтверждением 4 новых `day-cycle-*` сцен перед их будущим принятием как
baseline.