diff --git a/docs/specs/146-four-phase-sun-background.md b/docs/specs/146-four-phase-sun-background.md new file mode 100644 index 00000000..a5ce8c01 --- /dev/null +++ b/docs/specs/146-four-phase-sun-background.md @@ -0,0 +1,630 @@ +# Issue #146 — четырёхфазный фон «Следует за Солнцем» + +Статус: **ТЗ на ревью** +Дата: 2026-08-14 +Тип: `feature` · приоритет: `P1` · пользовательская ценность: 7/10 · сложность/риск: 8/10 + +Issue: [#146](https://github.com/Matysh/houseplan-card/issues/146) +Ветка: `issue/146-four-phase-sun-background` +Канонические документы: [SCOPE](../SCOPE.md), [SUN](../SUN.md), +[UX-MODES](../UX-MODES.md), [TOUCH-SUPPORT](../TOUCH-SUPPORT.md), +[ARCHITECTURE](../ARCHITECTURE.md), +[CONFIG-COMPATIBILITY](../CONFIG-COMPATIBILITY.md). + +Текст issue имеет приоритет над приложенными к нему `SPECIFICATION.md`, +`README.md` и интерактивным прототипом. Решения владельца Q1–Q3 и все defaults +приняты 2026-08-14 в issue #146. + +## 1. Сценарий и продуктовый контекст + +Основная персона — человек, который ежедневно смотрит на дом в View либо на +настенной kiosk-панели и должен одним взглядом узнавать сам план и текущее +состояние дома. Фон вокруг плана даёт спокойный временной контекст, но не имеет +права искажать цвета пола, стен, Glow, устройств или других данных. + +Это часть J1 из `docs/SCOPE.md`: House Plan существует прежде всего как +правдивый пространственный обзор дома. Новая функция декоративна, поэтому она +не становится единственным носителем состояния и не меняет доступность +управления. View и kiosk остаются полностью поддержанными touch-поверхностями. + +## 2. Что человек увидит до и после + +**До:** режим `daynight` непрерывно интерполирует один сплошной цвет по высоте +Солнца, требует настроенный север, а ночью уменьшает яркость всего плана до +10 %. У новых установок и пространств режим не включён автоматически. + +**После:** вокруг неизменного плана отображается одно из четырёх узнаваемых +окружений — рассвет, день, сумерки или ночь. Между фазами за 1100 ms плавно +меняются только градиент окружения, декоративный солнечный свет, внешняя +виньетка и alpha-aware контур плана. При доступном `sun.sun` состояние и свет +следуют реальному Солнцу; иначе работают по локальным часам браузера. Новые +установки и новые пространства начинают с режима «Следует за Солнцем», старые +неявные настройки сохраняют прежний статический вид. + +## 3. Проблема и подтверждённая техническая база + +1. `src/sun.ts::dayPhase()` возвращает непрерывный solid background, + `planDim` и warmth. Это не четырёхфазная модель из issue. +2. Full card применяет ночной `brightness()` к `.zoomwrap`, поэтому вместе с + окружением меняются сам план и все его live-слои. +3. `bg_mode: daynight` сейчас зависит от `north_deg` и валидного `sun.sun`; + без компаса либо сущности молча возвращается статический фон. +4. `sunStateOf()` читает только azimuth/elevation. Направление движения + (`attributes.rising`) ещё не участвует в выборе dawn/dusk. +5. Full card и `houseplan-space-card` рассчитывают фон разными render-путями, + а editor canvases намеренно остаются статическими. +6. Текущий fallback отсутствующего `bg_mode` — `static`. Простая смена fallback + на `daynight` изменила бы уже существующие планы без согласия пользователя. +7. Уже существуют независимые солнечные лучи из окон, Glow/spill, room fills, + hover, decor/backdrop, vacuum и mode-transition #101. Новый фон должен + интегрироваться с ними, а не заменить или перекрасить их. + +## 4. Решения владельца + +1. В диапазоне высоты между −6° и +6° признак + `sun.sun.attributes.rising` различает рассвет и сумерки: rising — `dawn`, + falling — `dusk`. Выше +6° — `day`, ниже −6° — `night`. +2. Если elevation или rising отсутствуют/некорректны, используется полный + fallback по локальному времени браузера: dawn 05:00–07:59, day + 08:00–17:59, dusk 18:00–20:59, night 21:00–04:59. +3. При валидном `sun.sun` и фаза, и положение декоративного фонового света + следуют реальным elevation/azimuth. Часовая дуга прототипа используется + только при полном fallback. Ночью отдельный световой круг скрыт. +4. Существующие пространства без явного выбора сохраняют прежний статический + вид через совместимую миграцию. Явный `daynight` получает новую + четырёхфазную семантику. Новые установки и каждое новое пространство + начинают с `daynight`; явный `static` автоматически не меняется. +5. Сам план не затемняется и не тонируется. Существующие Glow/spill, солнечные + лучи из окон, room fills/hover, decor/backdrop, vacuum и остальные live-слои + сохраняют действующее поведение. + +## 5. Scope + +В задачу входят: + +1. дискретная модель `dawn | day | dusk | night` для `bg_mode: daynight`; +2. реальный sun-source с полным локальным clock-fallback; +3. четырёхфазный градиент окружения, декоративный солнечный свет, виньетка и + alpha-aware контур по утверждённым visual tokens; +4. переход фаз за 1100 ms и `prefers-reduced-motion`; +5. одинаковая фаза и визуальные tokens в full View, kiosk и статической + `houseplan-space-card`; +6. совместимость с mode-transition View ↔ editor из #101; +7. новый default для новой установки, ручного создания пространства и + Floors/Areas onboarding; +8. однократная совместимая миграция старого неявного global default; +9. совместимость полного и per-space export/import; +10. обновление существующих RU/EN строк режима и подсказок; +11. unit, backend, production-bundle smoke, golden и performance coverage; +12. пользовательская/архитектурная документация, оба changelog и три + поставляемые bundle-копии. + +## 6. Non-scope + +В задачу не входят: + +- новый публичный mode token вместо `static | daynight`; +- ручной phase selector, видимый QA-control или пользовательское расписание; +- геолокация, внешний weather API, сетевой астрономический расчёт либо новые HA + service calls/entities; +- изменение существующей геометрии/порогов/цвета солнечных лучей из окон; +- настройка visual palette, длительности перехода или порогов −6/+6 в UI; +- окрашивание, затемнение, насыщенность, opacity либо blend самого плана; +- day/night окружение внутри Plan/Devices/Decor editors; +- пересмотр бумажного слоя, room fill, Glow, hover, decor, vacuum, + isometric/Labs или View ↔ editor UX; +- сохранение текущей вычисленной фазы в config/storage; +- canvas/WebGL только ради фонового градиента; +- production-обработка URL-параметра `?time=HH:MM`; +- изменение touch-контракта desktop-first редакторов. + +## 7. Контракт источника и выбора фазы + +### 7.1. Валидный real-sun sample + +Для **окружения** real-sun sample валиден, только если одновременно: + +- существует live-state `sun.sun`; +- `attributes.elevation` — конечное число; +- `attributes.azimuth` — конечное число, нормализуемое в `[0, 360)`; +- `attributes.rising` — настоящий boolean. + +Если любое условие нарушено, весь day-cycle — и фаза, и положение фонового +света — атомарно переходит на clock-fallback. Нельзя смешивать фазу от часов с +позицией из частично валидного `sun.sun`. + +Это отдельная проверка только для окружения. Действующий `sunStateOf()` и +существующие оконные лучи продолжают использовать azimuth/elevation по +`docs/SUN.md`: отсутствие `rising` не должно выключить уже работающие лучи. + +### 7.2. Фаза по реальному Солнцу + +Границы детерминированы так: + +| Условие | Фаза | +|---|---| +| `elevation <= -6` | `night` | +| `-6 < elevation < 6` и `rising === true` | `dawn` | +| `elevation >= 6` | `day` | +| `-6 < elevation < 6` и `rising === false` | `dusk` | + +На точной верхней границе состояние уже `day`, на точной нижней — уже `night`. +Azimuth wrap 359°→0° не меняет фазу. + +### 7.3. Полный clock-fallback + +Используются часы и часовой пояс браузера через локальный `Date`, без серверного +времени: + +| Локальные минуты | Фаза | +|---|---| +| `300 <= m < 480` (05:00–07:59) | `dawn` | +| `480 <= m < 1080` (08:00–17:59) | `day` | +| `1080 <= m < 1260` (18:00–20:59) | `dusk` | +| иначе (21:00–04:59) | `night` | + +DST и смену часового пояса даёт платформа `Date`. Интервал проверяет часы +каждые 30 s только пока используется fallback и вкладка видима. Дополнительно +состояние пересчитывается до первого видимого кадра, при `pageshow` и при +`visibilitychange` обратно в `visible`. Если фаза и позиционные tokens не +изменились, Lit/DOM update не запрашивается. + +### 7.4. Переключение источника + +Появление, исчезновение либо восстановление валидности `sun.sun` пересчитывает +весь day-cycle из одного snapshot. Текущая фаза не хранится как истина и не +может остаться от прежнего источника. Обычная смена источника получает тот же +1100 ms environment transition; fresh mount сразу начинает с правильного +состояния без вспышки default-day. + +## 8. Контракт декоративного фонового света + +Декоративный свет — pointer-inert слой окружения под планом. Он не является +оконным солнечным лучом и не использует `north_deg`: фон существует независимо +от ориентации дома. + +### 8.1. Real-sun position + +При валидном real-sun sample позиция вычисляется из реальных атрибутов: + +```text +x = 50 - sin(azimuth * pi / 180) * 42 // percent +y = 78 - clamp(elevation, 0, 90) / 90 * 64 // percent +opacityFactor = clamp((elevation + 6) / 12, 0, 1) +``` + +Так восток (около 90°) оказывается у левого горизонта, запад (около 270°) — у +правого, а северный и южный полдень проходят через центр без зависимости от +полушария. Отрицательная высота держит свет у горизонта; в `night` opacity +принудительно равна нулю. + +### 8.2. Clock-fallback arc + +Fallback повторяет дугу прототипа: + +```text +progress = (minutes - 300) / (1260 - 300) +x = 8 + progress * 84 +y = 78 - sin(progress * pi) * 64 +opacityFactor = max(.18, min((minutes - 300) / 120, + (1260 - minutes) / 120, 1)) +``` + +Формулы применяются только в диапазоне 05:00 ≤ time < 21:00; вне его opacity +равна нулю. Позиция обновляется той же 30-секундной fallback-проверкой, не +пересчитывая план, стены, room geometry или устройства. + +### 8.3. Вид и слой + +Свет — мягкий radial gradient без жёсткой окружности, размером 250 CSS px в +эталонном desktop layout; на меньшей поверхности он ограничивается размером +окружения и не создаёт overflow/scroll. Цвет задаётся фазой, позиция и opacity — +источником выше. В `night` элемент может оставаться в DOM, но его вычисленная +opacity обязана быть 0 и ни один видимый световой круг не допускается. + +## 9. Визуальный контракт четырёх фаз + +Окружение строится из вертикального linear gradient и мягкого radial horizon у +нижней части сцены. Значения ниже — канонические tokens прототипа; отклонение +требует нового решения владельца, а не локального «подбора на глаз». + +| Фаза | top | bottom | horizon | sun | vignette | +|---|---|---|---|---|---| +| `dawn` | `#aabdd1` | `#e8c8b7` | `rgba(255,201,156,.56)` | `rgba(255,188,125,.78)` | `rgba(65,72,99,.21)` | +| `day` | `#dce9ef` | `#cbdce3` | `rgba(255,245,220,.45)` | `rgba(255,239,190,.72)` | `rgba(65,91,105,.16)` | +| `dusk` | `#48536c` | `#9a7380` | `rgba(242,156,114,.34)` | `rgba(255,167,113,.55)` | `rgba(20,26,44,.39)` | +| `night` | `#111a27` | `#1f2f3e` | `rgba(79,120,151,.16)` | transparent | `rgba(3,8,14,.58)` | + +Alpha-aware контур применяет три zero-offset `drop-shadow` либо визуально +эквивалентный эффект к **одному каноническому plan-paper composite**, не к +каждой комнате/стене отдельно: + +| Фаза | near `0 0 1px` | mid `0 0 5px` | far `0 0 10px` | +|---|---|---|---| +| `dawn` | `rgba(74,57,61,.25)` | `rgba(255,238,224,.40)` | `rgba(255,224,202,.18)` | +| `day` | `rgba(45,62,71,.28)` | `rgba(255,255,255,.42)` | `rgba(255,255,255,.20)` | +| `dusk` | `rgba(238,219,225,.40)` | `rgba(229,207,218,.26)` | `rgba(215,190,205,.12)` | +| `night` | `rgba(218,238,249,.56)` | `rgba(174,215,238,.30)` | `rgba(136,194,226,.14)` | + +Контур повторяет внешний alpha-контур общего бумажного footprint, не имеет +смещения, не создаёт внутренних швов между комнатами и не обводит устройства, +labels, hover или live-эффекты. Для image-plan канонический footprint — его +действующий opaque paper rect; для drawn-plan — объединённая по одному group +совокупность room paper polygons. Пустой drawn space без комнат не получает +ложный прямоугольный контур. + +## 10. Инвариант неизменного плана + +Во всех четырёх фазах к plan/content tree запрещено добавлять: + +- `brightness`, `contrast`, `saturate`, `sepia`, `hue-rotate` или иной + color filter; +- opacity родителя, `mix-blend-mode` или цветной overlay поверх плана; +- смену paper/floor/room fill цвета из-за фазы; +- изменение яркости/цвета devices, labels, decor/backdrop, vacuum, Glow/spill, + hover или оконных солнечных лучей. + +Действующий `dayPhase().planDim` удаляется из day-cycle render и из target +brightness mode-transition #101. Plan pixels внутри plan-paper alpha footprint +до и после смены фазы должны совпадать; разрешено меняться только внешнему +zero-offset outline за границей footprint. + +## 11. Переходы и surfaces + +### 11.1. Phase transition + +При смене фазы ровно 1100 ms с easing +`cubic-bezier(.22, .61, .36, 1)` анимируются только: + +- environment top/bottom/horizon; +- цвет/позиция/opacity декоративного света; +- внешняя vignette; +- три цвета alpha-aware outline. + +Нельзя fade всего экрана или самого плана. Реализация обязана использовать +действительно интерполируемые CSS primitives либо environment-layer crossfade; +простая декларация `transition` на неанимируемом gradient не считается +выполнением. При `prefers-reduced-motion: reduce` длительность практически +нулевая (`<= 0.01 ms` либо `transition: none`). + +### 11.2. Full View, kiosk и static card + +Full View и kiosk используют один environment model. Статическая +`houseplan-space-card` получает ту же effective phase, palette, свет, outline и +fallback lifecycle; оконные wedges в static card не добавляются — действующее +ограничение `docs/SUN.md` сохраняется. + +### 11.3. Editors и #101 + +Plan/Devices/Decor editors сохраняют действующий editor background и не +получают четырёхфазное окружение. При View ↔ editor переходе #101: + +1. исходный и целевой кадр используют один атомарный phase/source snapshot; +2. сам plan composite не меняет brightness/color; +3. environment transition не перехватывает pointer/focus и не оставляет + скрытый слой после commit/cancel; +4. при возвращении в View актуальная фаза пересчитывается до целевого кадра, + включая возврат после долгой паузы. + +### 11.4. Существующие live-эффекты + +Glow/spill, room fills/hover, decor/backdrop, vacuum, labels/devices и оконные +лучи сохраняют порядок слоёв, opacity, blend и lifecycle. `north_deg` продолжает +управлять только ориентационно зависимыми оконными лучами; `daynight` background +больше не выключается из-за отсутствия компаса. + +## 12. Модель данных, defaults, migration и import/export + +### 12.1. Persisted schema + +Публичная schema не расширяется: global и per-space +`settings.bg_mode: 'static' | 'daynight'`, per-space null/absence = inherit. +Вычисленная фаза, источник и позиция света не сохраняются. Backend продолжает +принимать отсутствие `bg_mode` ради legacy compatibility. + +Runtime fallback отсутствующего effective token остаётся `static`; новая +семантика достигается материализованными defaults, а не переопределением +отсутствующего поля. + +### 12.2. Новая установка и новые пространства + +- Новый `DEFAULT_CONFIG` материализует global `settings.bg_mode: 'daynight'`. +- Ручное Create space материализует per-space `bg_mode: 'daynight'`. +- Каждое пространство, созданное Floors/Areas onboarding, материализует + per-space `bg_mode: 'daynight'`. +- Открытие и сохранение существующего space dialog не меняет его mode без + действия пользователя. +- Явный `static` на любом уровне никогда не переписывается автоматически. + +Per-space materialization нужна даже при новом global default: добавленное +позднее пространство начинает с `daynight`, в том числе внутри старой +установки, чей global default мигрирован в `static`. + +### 12.3. Однократная миграция существующего store + +Backward-compatible storage migration выполняется один раз и идемпотентно: + +1. если в существующем config global `settings.bg_mode` отсутствует либо + невалиден, он материализуется как `static`; +2. валидный global `static` или `daynight` сохраняется; +3. все валидные per-space overrides сохраняются без переписывания; +4. spaces без override продолжают наследовать уже материализованный global; +5. migration сохраняет неизвестные поля, не меняет layout и не создаёт + config-update storm; +6. повторный старт не создаёт новую запись/revision. + +### 12.4. Export/import + +- Full export после миграции содержит явный global mode; legacy full import без + него материализуется как `static` до preview/apply. +- Per-space export материализует в экспортируемом space его **effective** mode, + чтобы перенос не зависел от отсутствующего global settings блока. +- Legacy per-space import без mode материализуется как `static` до merge. +- Явные `static`/`daynight` сохраняются при same-instance и foreign import. +- Import preview показывает тот же итоговый mode, который будет применён; + preview не пишет storage. +- Никакой импорт не превращается в «новое пространство по default»: + импортированная семантика имеет приоритет над default ручного Create. + +## 13. UX, i18n, accessibility и touch + +Видимых новых controls нет. Действующий selector остаётся двухпозиционным: +`static` и `daynight`; color picker виден только для `static`. + +Обновляются существующие RU/EN keys без добавления третьего режима: + +- `gs.bg_daynight`: «Следует за Солнцем» / “Follows the Sun”; +- `gs.bg_daynight_hint`: четыре фазы, real sun и локальный fallback, без + требования компаса; +- `gs.sun_missing` и `gs.north_hint`: отсутствие sun/севера отключает оконные + лучи, но не clock-driven background. + +Фаза декоративна: она не добавляет announcement, focus target, toast или +semantic state; не меняет размеры сцены и не служит единственным носителем +информации. `forced-colors` сохраняет функциональную читаемость plan/devices; +декоративные environment layers могут быть упрощены либо отключены. + +View, kiosk и static card полностью поддерживаются мышью, touch и без pointer: +новые слои имеют `pointer-events: none`, не перехватывают tap/swipe/pinch и не +меняют hit targets. Editor остаётся desktop-first и не получает новых действий. + +## 14. Архитектурный и performance-контракт + +1. Один pure resolver возвращает phase, source (`sun | clock`) и декоративные + position/opacity tokens. Full/static surfaces не копируют пороги и формулы. +2. Palette хранится одной typed таблицей. CSS и тесты не получают расходящиеся + hard-coded варианты. +3. Real-sun resolver запускается только на релевантном изменении sun snapshot, + config/space или lifecycle catch-up; прочие `hass` ticks не меняют DOM. +4. Clock timer существует только в fallback, не чаще 30 s, не выполняет + необязательную работу в hidden document и очищается в `disconnectedCallback`. +5. Не создаётся `requestAnimationFrame` loop, canvas/WebGL, geometry/layout + polling, новая сеть, service call или storage write. +6. Phase/source/position update меняет только bounded environment state/style. + Rooms, walls, openings, Glow barriers, devices и backdrop geometry не + пересчитываются. +7. Alpha-aware outline применяется к одному plan-paper composite; per-room + filter multiplication запрещён. +8. Environment DOM count постоянен относительно rooms/devices и не растёт при + каждой фазе или visibility cycle. +9. #101 и cold-start #131 сохраняют первый полный кадр: не допускается второй + пустой render ради вычисления локального времени. +10. Таргетированный performance smoke доказывает отсутствие geometry/device + rebuild на clock tick и обычном нерелевантном hass tick. Перед бетой на + exact SHA обязательны существующие performance smoke и Full Performance без + ослабления budgets. + +## 15. Acceptance criteria + +- **AC1 (`unit`; разработчик):** real-sun resolver детерминированно выдаёт + `night/dawn/day/dusk` на обеих сторонах −6/+6, точных границах, при rising и + falling; azimuth wrap не меняет фазу. +- **AC2 (`unit` + production-bundle smoke; разработчик):** отсутствие/garbage + любого из elevation/azimuth/rising атомарно включает clock-fallback с точными + границами 05:00/08:00/18:00/21:00; валидность real sample восстанавливает весь + sun-source без смешивания данных. +- **AC3 (`unit` + targeted smoke; разработчик):** real-sun позиция следует + elevation/azimuth, fallback — утверждённой дуге 05:00→13:00→21:00, а night + имеет opacity 0. Ни один update не пересчитывает plan geometry. +- **AC4 (`golden` + computed-style smoke; владелец/ревьюер):** dawn/day/dusk/ + night используют точные palette и outline tokens раздела 9; ночью нет + видимого светового круга, а светлый дневной и тёмный ночной план отделены от + окружения. +- **AC5 (`pixel regression` + code review; разработчик/ревьюер):** внутри + plan-paper footprint пиксели plan, floors, room fills, Glow/spill, + devices/labels, decor/backdrop, vacuum, hover и оконных лучей совпадают между + четырьмя фазами; никакой phase-dependent plan filter/opacity/blend не остаётся. +- **AC6 (`targeted smoke`; разработчик):** смена фазы анимирует только environment + и outer outline за 1100 ms с заданным easing; plan не мигает. Reduced motion + завершает переход практически мгновенно. +- **AC7 (`production-bundle smoke` + golden; разработчик/владелец):** full View, + kiosk и static card получают одну effective phase/palette; editors остаются + прежними, а #101 transition не оставляет stale/interactive environment layer. +- **AC8 (`unit` + smoke; разработчик):** `daynight` работает без `north_deg` и + без `sun.sun` через clock fallback; оконные лучи по-прежнему требуют свои + действующие sun/north gates и не меняют геометрию/пороги. +- **AC9 (`backend migration tests`; разработчик):** существующий отсутствующий + global mode мигрирует в `static` ровно один раз; валидные global/per-space + modes и неизвестные поля сохраняются; повторный старт не пишет store/rev. +- **AC10 (`frontend unit` + `backend/import tests` + smoke; разработчик):** новая + установка, manual Create и Floors/Areas create материализуют `daynight`; + существующий Edit не меняет mode сам; явный `static` остаётся static. +- **AC11 (`backend/import-export tests`; разработчик):** full/space export и + legacy/current import выполняют контракт §12.4, preview совпадает с apply, а + preview не пишет storage. +- **AC12 (`i18n test` + accessibility/touch smoke; разработчик):** RU/EN тексты + описывают четыре фазы и fallback; environment pointer-inert, не двигает focus, + не меняет размеры/hit targets и не ломает tap/swipe/pinch в View/kiosk. +- **AC13 (`lifecycle smoke`; разработчик):** fallback пересчитывается сразу, + каждые 30 s только в visible state, на `pageshow` и visible-return; timer и + listeners очищаются при disconnect, повторное подключение не дублирует их. +- **AC14 (`performance smoke` + code review; разработчик/ревьюер):** sun/clock + update меняет bounded environment state без room/device/geometry rebuild, + RAF-loop, canvas, сети, HA calls или storage writes; DOM/timer count bounded. +- **AC15 (`typecheck` + `unit` + `build` + docs review; разработчик):** локальные + implementation gates зелёные, обе пользовательские документации/changelog и + три bundle-копии обновлены в одном `User-Visible: yes` коммите. +- **AC16 (`exact-SHA Linux gates`; Claude/релиз-инженер):** перед бетой зелёны + Validate, полный smoke, reviewed golden и Full Performance на одном SHA без + ослабления budgets; security/network/schema verdict явно записан в code review. + +## 16. План автотестов и визуального review + +### 16.1. Unit/frontend + +Добавить pure-helper coverage: + +1. elevation `-6.1/-6/-5.9/5.9/6/6.1` при обоих rising values; +2. NaN/infinity/string/missing для elevation, azimuth и rising; +3. fallback минуты `299/300/479/480/1079/1080/1259/1260`; +4. real position east/south/west/north и elevation below horizon/0/45/90; +5. fallback position/opacity в 05:00, 07:00, 13:00, 19:00, 21:00; +6. palette completeness и night opacity 0; +7. mode inheritance с compatibility fallback `static`; +8. отсутствие влияния нового rising requirement на sun-ray helpers. + +### 16.2. Backend и compatibility + +Покрыть storage migration: missing, static, daynight, mixed per-space, +malformed legacy, unknown fields, idempotent second load и rev/event count. + +Расширить import/export matrix: legacy full/space без mode, current full/space с +обоими modes, inherited export materialization, same/foreign source, preview +versus apply и import into target с противоположным global mode. + +### 16.3. Targeted production-bundle smoke + +Обновить sun/background smoke либо добавить отдельный four-phase smoke на +собранном bundle: + +1. full View, kiosk и `houseplan-space-card` проходят четыре deterministic + real-sun phase; +2. отдельная matrix проходит clock fallback и source recovery; +3. computed styles проверяют palette, outline, light visibility и transition; +4. pixel probes/masked screenshots доказывают неизменность plan interior; +5. existing window rays работают с north, а background — без north; +6. editor и View↔editor transition не получают stale layer; +7. visibility/pageshow/timer cleanup и reduced motion проверяются под fake clock; +8. manual/Floors create и existing Edit проверяют persisted modes; +9. counters доказывают отсутствие geometry/device rebuild и storage/network + activity на fallback tick. + +Targeted smoke пишется вместе с кодом; полный smoke-suite запускается перед +бетой, не в implementation loop. + +### 16.4. Golden + +Нужны новые deterministic Linux golden-сцены full View для `dawn`, `day`, +`dusk`, `night` и static-card сцены для двух контрастных крайних состояний +`day/night`. Одинаковые fixture plan/live state/camera исключают посторонний +diff; reduced motion включён. + +Golden artifact ревьюится владельцем/Claude как единый набор: соответствие +прототипу, отсутствие plan tint/dim, световой круг не виден ночью, outline не +создаёт per-room seams. Baseline принимается только штатной командой +`golden:accept -- --reviewed` из полного Linux-артефакта и отдельным коммитом с +обязательными release/baseline trailers по runbook; принять baseline ради +зелёного CI нельзя. + +## 17. План реализации + +1. Ввести typed day-cycle resolver/palette рядом с sun pure logic, не меняя + оконную ray geometry. +2. Подключить единый environment model и lifecycle к full/static surfaces; + убрать phase-dependent plan dim и compass gate background. +3. Реализовать environment layers, decorative light, vignette, composite + outline, 1100 ms/reduced-motion и интеграцию #101. +4. Материализовать новые defaults, storage migration и import/export + compatibility. +5. Обновить selectors/hints i18n, документацию и changelog. +6. Добавить unit/backend/targeted production-bundle smoke и deterministic + golden scenes. +7. Выполнить implementation-loop `typecheck`, `unit`, `build`; остальные гейты + — на пре-релизе по команде владельца. + +Точные имена helper-функций, CSS classes и private fields не являются +продуктовым контрактом. + +## 18. Release-артефакты + +Изменение пользовательское: implementation-коммит имеет `User-Visible: yes` и +в том же коммите обновляет: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`; +- `docs/SUN.md` — новый источник истины по фазам, fallback, визуалу и + независимости от компаса; +- `docs/USER-GUIDE.ru.md` — выбор режима, четыре фазы, defaults и fallback; +- `docs/ARCHITECTURE.md` — resolver/lifecycle/layers и full/static parity; +- `docs/CONFIG-COMPATIBILITY.md` — storage/default/import migration; +- `docs/TESTING.md` — targeted smoke/golden matrix; +- RU/EN i18n и три поставляемые bundle-копии. + +Перед бетой на exact SHA обязательны: + +- Linux Validate; +- полный smoke-suite; +- reviewed/accepted golden artifact без необъяснённого diff; +- performance smoke и Full Performance без ослабления budgets; +- code review с явным negative security/network/HA-call verdict. + +Новый отдельный security report не требуется: feature не принимает HTML/URL, +не добавляет network/storage input или HA action. Публикация проходит через +обычную бету до stable; issue закрывает владелец пачкой после опубликованной беты. + +## 19. Риски и меры + +| Риск | Вероятность / влияние | Мера | +|---|---|---| +| План незаметно остаётся dim/tinted старым filter | средняя / высокая | AC5 pixel mask, удаление planDim consumers, four-phase golden | +| Старый план без mode внезапно включит daynight | средняя / высокая | materialized storage migration в static, idempotence/backend tests | +| Новое пространство в старой установке наследует static | высокая / средняя | explicit per-space daynight в manual и Floors create, AC10 | +| Partial sun sample смешивается с clock position | средняя / средняя | атомарный resolver/source, garbage matrix AC2 | +| CSS gradient прыгает вместо перехода | средняя / средняя | проверяемые intermediate styles/pixels, animatable primitive/crossfade | +| Outline создаёт швы по комнатам или обводит devices | средняя / высокая | один plan-paper group, full/static golden и DOM assertion | +| 30 s timer течёт/дублируется | средняя / средняя | fallback-only lifecycle, disconnect/reconnect smoke | +| Background начинает зависеть от north и молчит | средняя / высокая | отдельные gates background/rays, no-north smoke | +| #101 оставляет stale слой либо pointer blocker | средняя / высокая | mode-transition targeted smoke, pointer-events none | +| Import наследует target mode вместо исходного | средняя / высокая | effective space export + legacy import materialization matrix | +| Visual layers ухудшают слабое устройство | низкая / высокая | bounded layers, один outline filter, performance exact-SHA gates | + +## 20. Откат + +Пользовательский немедленный откат — выбрать `static` глобально либо для +пространства; environment timer/layers становятся инертны без потери данных. + +Технический откат — revert implementation-коммита вместе с тестами, +документацией, changelog и bundle-копиями. Миграция только материализует ранее +эффективный `static`, поэтому старая версия полностью читает результат; новые +явные `daynight` также являются старым валидным token. Обратная миграция и +удаление полей не нужны. Если default новой установки требуется временно +отключить без полного revert, вернуть creation defaults в `static`; сохранённые +пользовательские choices не переписывать. + +## 21. Принятые технические предположения — можно менять без продуктового ревью + +1. Точная граница считается `<= -6 => night`, `>= 6 => day`; только открытый + интервал различает rising/falling. +2. Environment использует отдельный strict real-sun validator; ray API не + расширяется обязательным `rising`. +3. Real decorative position использует формулы §8.1: horizontal проекция через + `sin(azimuth)`, vertical — линейно по положительной elevation. Это + декоративное отображение, не плановая координата и не зависит от north. +4. Размер light можно выразить через bounded `clamp()` вместо ровно 250 px на + маленьких карточках, сохранив desktop reference и visual intent. +5. Конкретный способ плавной смены gradient — registered custom properties или + два bounded environment layers; AC важнее механизма. +6. QA не читает production `?time`: fake clock/sun внедряется только в unit, + demo и smoke harness. Пользовательского QA-control нет. +7. Storage migration рекомендуется реализовать bump minor version с проверкой + формы config-store document; точный helper свободен при соблюдении AC9. +8. Per-space export может материализовать effective mode в копии export payload, + не меняя live config. +9. Один shared fallback ticker допустим вместо timer на карточку, если lifecycle, + isolation нескольких карточек и bounded listener count доказаны тестами. +10. DOM может хранить фазу как `data-day-cycle` либо typed class; публичной + schema/DOM API это не становится. +11. Контур строится вокруг действующего канонического opaque paper composite: + image rect либо общего group room papers; новый анализ alpha пикселей + загруженного raster-файла не требуется. +12. Открытых продуктовых вопросов нет; все Q1–Q3 и defaults приняты владельцем + 2026-08-14. diff --git a/docs/specs/README.md b/docs/specs/README.md index 44cb3333..941efd2e 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -47,6 +47,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#98](https://github.com/Matysh/houseplan-card/issues/98) Единая система пульсаций и активностей устройства | [098-device-pulse-system.md](098-device-pulse-system.md) | | [#131](https://github.com/Matysh/houseplan-card/issues/131) Полный первый кадр View у read-only-пользователя | [131-readonly-cold-start.md](131-readonly-cold-start.md) | | [#138](https://github.com/Matysh/houseplan-card/issues/138) Автозамыкание комнаты по существующей стене | [138-adjacent-room-autoclose.md](138-adjacent-room-autoclose.md) | +| [#146](https://github.com/Matysh/houseplan-card/issues/146) Четырёхфазный фон «Следует за Солнцем» | [146-four-phase-sun-background.md](146-four-phase-sun-background.md) | ## P2