# ТЗ #21 — безопасные пользовательские цвета - Issue: https://github.com/Matysh/houseplan-card/issues/21 - Приоритет: P2 - Статус: реализовано; целевой gate v1.61.0-beta.1 пройден, ожидается публикация - Блокирует цветовые части #50 и #56; SVG sanitization из #51 остаётся отдельной задачей ## Проблема Новые записи проходят backend-валидацию, но старый, импортированный или вручную изменённый store может попасть во frontend без повторной проверки. Подтверждённый разрыв: `marker.ripple_color` доходил до CSS custom property как часть строки inline-style. Lit не позволяет выйти из HTML-атрибута, но строка вида `red;position:fixed` могла добавить соседние CSS declarations. ## Единый контракт Все сохраняемые цвета Houseplan имеют только вид `#RRGGBB`: - ровно `#` и шесть шестнадцатеричных цифр; - регистр букв любой; - без пробелов, короткого `#RGB`, имени цвета, `rgb()`, `var()`, `url()` и других CSS-функций; - неверное значение не исправляется и не записывается автоматически: при чтении используется безопасный штатный fallback. Это соответствует уже существующим color picker и backend-схеме. Расширять форматы цвета в рамках #21 не требуется. ## Реализация ### Frontend Один pure helper `safeStoredColor(value, fallback)` является общей границей для всех цветов из сохранённой конфигурации: - цвет и фон пространства; - глобальный фон и палитра заливок, включая стены и Glow; - обводка и заливка декоративных объектов и текста; - `marker.ripple_color`; - компонент выбора цвета. Проверка выполняется при проекции данных для рендера, поэтому защищает и старый store, записанный до этого правила. Невалидный пользовательский ripple не должен перекрывать безопасный динамический цвет лампы. HA может отдавать `rgb_color`. Это не сохраняемый пользовательский цвет: приложение принимает три конечных числа, ограничивает каждый канал диапазоном 0–255, округляет и само создаёт каноническую строку `rgb(R, G, B)`. На последней границе inline-style разрешены только строгий `#RRGGBB` и именно такой сгенерированный `rgb()`. ### Backend Все поля цвета используют один `_COLOR = ^#[0-9a-fA-F]{6}$`. Поведение API не меняется: корректный hex принимается, остальные формы отклоняются. ### Совместимость - валидные планы визуально не меняются; - невалидный старый цвет заменяется только в runtime на штатный fallback; - исходный config не мутируется при чтении; - при следующем обычном редактировании color picker показывает безопасный fallback, а сохранение записывает валидный цвет. ## Обязательные проверки перед пре-релизом 1. Unit corpus: валидный mixed-case hex; short hex, whitespace, named/rgb, `;`, braces, comment, backslash, newline, `url()`, overlong и non-string. 2. Проекция устройства: hostile `ripple_color` заменяется динамическим цветом включённой RGB-лампы либо `null`, если такого цвета нет. 3. Последняя style-граница не выводит hostile ripple declaration. 4. Decor/space/fill resolvers используют fallback и не меняют входной объект. 5. Backend одинаково отклоняет hostile значение во всех цветовых полях. 6. Browser smoke на импортированном hostile config: нет overlay, внешнего запроса, новой style declaration и ошибки рендера. ## Критерии приёмки - Ни одно значение цвета из store не попадает в CSS/SVG без общей проверки. - Все новые записи принимают только `#RRGGBB`. - Динамический цвет HA остаётся рабочим и не может содержать произвольный CSS. - Визуальное поведение валидных существующих планов не изменено. - Целевые frontend/backend тесты и браузерные regression smokes выполнены в gate v1.61.0-beta.1; issue закрывается после зелёного exact-SHA CI и проверки release assets.