v9.43: pre-graph-cleanup baseline (zebra, uuid copy, theme fix, delete policy)

This commit is contained in:
Naeel
2026-04-17 08:51:07 +03:00
parent 38efc70bed
commit b778f5554c
7 changed files with 940 additions and 124 deletions
+288
View File
@@ -210,3 +210,291 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=
Почему:
- так исключаются конфликты сред и ложные результаты;
- единая точка выполнения (ВМ) дает предсказуемость и воспроизводимость.
---
## 13. Дисциплина загрузки таблицы (актуально)
Обязательная модель работы таблицы:
- В начале загрузки читаем данные инстансов и их параметры только из API.
- Строка таблицы появляется сразу после чтения параметров конкретного инстанса.
- Параллельные обращения к внешнему API запрещены: только последовательная обработка.
- После завершения загрузки повторных обращений к API нет.
- Новые обращения к API выполняются только по кнопке Обновить.
Требования к UX загрузки:
- В верхней строке перед кнопкой Обновить показывать прогресс в формате Загрузка X/Y.
- После завершения загрузки показывать Актуально на ЧЧ:ММ:СС.
- Рядом с меткой актуальности показывать время загрузки последнего обновления.
Требования к таблице (операторский режим):
- После чтения параметров конкретного инстанса из API сразу добавлять его строку в таблицу.
- Добавить узкий столбец удаляемости сразу после статуса.
- Зеленый индикатор показывать только если инстанс в suspended и прошло 14+ дней с входа в suspended (по данным API).
- В раскрытой строке suspended-инстанса показывать пояснение по удаляемости.
- Блоки Input/Output параметров не растягивать на всю ширину; сделать компактный читаемый layout.
- Порог удаляемости, паттерны операций и пользовательские тексты хранить в конфиге фронта (без хардкода в вычислениях).
## 14. Журнал решений и хронология (без скрытых рассуждений)
### Шаг A — Режим загрузки таблицы
- Цель: сделать поведение предсказуемым для оператора и убрать лишние запросы.
- Входные ограничения: читать данные только из API; после загрузки работать из памяти; новые запросы только по Обновить.
- Варианты:
- A1: догружать детали по клику строки.
- A2: читать детали сразу в стриме и больше не дергать API.
- Выбранный вариант: A2.
- Причина выбора: минимальная фрагментация данных, прозрачный UX, отсутствие неожиданной сетевой активности.
- Проверка: поток отдает progress и done-событие; во фронте удалена догрузка деталей по клику.
### Шаг B — Последовательные вызовы API
- Цель: исключить параллельные обращения к API.
- Входные ограничения: запрет на параллельные запросы.
- Варианты:
- B1: batch-параллелизм с ограничением concurrency.
- B2: строго последовательный обход uid.
- Выбранный вариант: B2.
- Причина выбора: полное соответствие ограничению и упрощение отладки.
- Проверка: убраны gather/create_task/Semaphore для стрим-логики.
### Шаг C — Удаляемость suspended > 14 дней
- Цель: добавить операторский индикатор удаления без лишней визуальной нагрузки.
- Входные ограничения: считать только по API-данным.
- Варианты:
- C1: считать от dtState всегда.
- C2: сначала искать suspend-операции, затем fallback на поля detail.
- Выбранный вариант: C2.
- Причина выбора: меньше ложных срабатываний при изменениях, не связанных с suspend.
- Проверка: индикатор и пояснение показываются только для suspended; порог 14+ дней.
### Шаг D — Немонолитная, но умеренная структура
- Цель: не превращать файл в монолит и одновременно не перегрузить архитектуру.
- Входные ограничения: без чрезмерного рефакторинга.
- Варианты:
- D1: полный вынос JS в модули/файлы.
- D2: локальная модульность внутри файла через конфиг и мелкие функции.
- Выбранный вариант: D2.
- Причина выбора: безопасно для текущей стадии, быстро настраивается, меньше риск поломок.
- Реализация:
- Конфиг политики/текстов вынесен в DASHBOARD_CFG.
- Общие шаблоны строк через fmtText.
- Рендер таблицы разделен на renderDataRow и renderDetailRow.
- Константа TABLE_COLSPAN и состояние TABLE_STATE исключают магические значения.
### Шаг E — Backend как источник вычисляемых метаданных
- Цель: убрать дублирующую бизнес-логику из фронта и сделать поведение единообразным.
- Входные ограничения: без избыточного рефакторинга, только практичный шаг.
- Варианты:
- E1: оставить расчеты удаляемости только во фронте.
- E2: считать удаляемость на backend и отдавать в stream-строке как мета-поля.
- Выбранный вариант: E2.
- Причина выбора: policy централизована на сервере, фронт становится проще и легче переконфигурируется.
- Реализация:
- В backend добавлен конфиг DELETE_POLICY и функции вычисления suspend-возраста.
- В stream добавлено поле _meta.deleteCandidate.
- Фронт сначала применяет backend _meta; клиентский fallback использовался временно и отключен на шаге G.
### Шаг F — Локальный snapshot таблицы в браузере
- Цель: после перезагрузки страницы не обращаться к API до явного нажатия Обновить.
- Входные ограничения: решение должно быть легким и не монолитным.
- Варианты:
- F1: только RAM (теряется при reload).
- F2: локальный snapshot per user+env в browser storage.
- Выбранный вариант: F2.
- Причина выбора: быстрый старт UI после reload и автономная работа без нового API-вызова.
- Реализация:
- Добавлен snapshot storage key на основе user+env.
- После успешного полного refresh snapshot сохраняется.
- На init сначала пробуется восстановление snapshot; API вызывается только если snapshot отсутствует.
- На logout snapshot очищается.
### Шаг G — Backend-only deleteability + snapshot schema v2
- Цель: исключить расхождение бизнес-правил между клиентом и сервером и безопасно версионировать локальный кэш.
- Входные ограничения: без тяжелого рефакторинга.
- Реализация:
- Клиентский fallback вычисления удаляемости отключен в stream-обработке.
- Поля `_canDelete`/`_suspendDays` инициализируются из backend `_meta.deleteCandidate` (или остаются нулевыми по умолчанию).
- Формат snapshot переведен на `version=2` и `schema=instance-stream-v2`.
- Ключ snapshot обновлен до `dashboard_snapshot_v2`, чтобы отделить новый формат от старого.
### Шаг H — Релиз v9.22 (build/push/deploy + верификация)
- Цель: доставить изменения шага G в кластер и подтвердить фактическую версию рантайма.
- Реализация:
- Собран образ `naeel/cloud-dashboard:v9.22`.
- Выполнен push в Docker Hub.
- Применен `k8s/deployment.yaml` в namespace `terra`.
- Rollout завершен успешно.
- Проверено соответствие версии в deployment и pod.
- Проверка:
- `spec.image`: `naeel/cloud-dashboard:v9.22`.
- `pod.image`: `naeel/cloud-dashboard:v9.22`.
- `pod.imageID`: `docker.io/naeel/cloud-dashboard@sha256:18e87ff592a333cc280fd2af13390f47b111616b8da31f2e32d7ac24e3a3a7ea`.
- `pod` статус: `Running`, `Ready=true`, `Restarts=0`.
- Внутренний smoke-check: `GET http://127.0.0.1:8080/` -> `HTTP 200`.
- Внешний smoke-check (ingress `terra.k8c.ru`): `HTTPS /` -> `200`, `HTTP /` -> `308`.
### Шаг I — Добавлен столбец UUID после Имени (v9.29)
- Цель: сделать идентификацию инстанса в таблице мгновенной без раскрытия строки.
- Реализация:
- В таблицу добавлен новый sortable-столбец `UUID` сразу после `Имя`.
- В рендере строк добавлена ячейка `instanceUid` (моноширинный стиль).
- `TABLE_COLSPAN` увеличен с `8` до `9`.
- Обновлены мобильные индексы скрываемых колонок (`nth-child`) после сдвига структуры.
- Выполнен version bump: .naeel/cloud-dashboard:v9.29`.
### Шаг J — Фикс `renderDataRow is not defined` (v9.30)
- Цель: убрать runtime-ошибку в таблице после добавления UUID-колонки.
- Реализация:
- `renderDataRow` и `renderDetailRow` вынесены из `toggle()` в общий scope.
- Рендер таблицы (`render`) снова вызывает доступные функции без `ReferenceError`.
- Выполнен version bump: .naeel/cloud-dashboard:v9.30`.
### Шаг K — Перенос светофора в колонку Изменён (v9.31)
- Цель: убрать отдельный столбец индикатора и показывать только полезный сигнал.
- Реализация:
- Удален отдельный столбец светофора из таблицы.
- Зеленая точка теперь рисуется после времени в колонке `Изменён` только для eligible (`suspended` > 14 дней, по backend-метаданным).
- Серые точки полностью убраны.
- `TABLE_COLSPAN` обновлен с 9 до 8.
- Обновлены мобильные индексы скрываемых колонок.
- Выполнен version bump: .naeel/cloud-dashboard:v9.31`.
### Шаг L — Возврат потокового вывода строк (v9.32)
- Цель: показывать строки таблицы по мере загрузки stream, без ожидания `_done`.
- Реализация:
- Исправлен `scheduleRender()`: вместо perpetual-debounce применяется throttle (если таймер уже запущен, новый не ставится).
- UI теперь обновляется регулярно в ходе потока, а не откладывается до конца непрерывного чтения.
- Выполнен version bump: .naeel/cloud-dashboard:v9.32`.
### Шаг M — Левое выравнивание 4 колонок в Параметрах (v9.33)
- Цель: сделать `Input/Output` читабельным в табличном стиле без центрирования.
- Реализация:
- `params-grid` переведен на `max-content` колонки и выравнивание влево.
- `p-item` переведен на grid из 2 колонок (`key` + `value`) с увеличенным фиксированным зазором между ними.
- `p-key` и `p-val` выровнены влево.
- Ширина колонок определяется содержимым; при реальном переполнении включается перенос (`overflow-wrap: anywhere`).
- Выполнен version bump: .naeel/cloud-dashboard:v9.33`.
### Шаг N — Ширина колонки имен: max + 10ch (v9.34)
- Цель: сделать значения читаемыми по единой вертикали и увеличить отступ от имен.
- Реализация:
- Для каждой секции (`Input`/`Output`) вычисляется максимальная длина имени параметра.
- Ширина колонки имен задается формулой: `maxKeyLen * 1ch + 10ch`.
- Значения начинаются строго от единой левой границы value-колонки в каждой секции.
- Выполнен version bump: .naeel/cloud-dashboard:v9.34`.
### Шаг O — Output +20 и copy-иконка параметров (v9.35)
- Цель: повысить читаемость `Input/Output` и ускорить копирование значений.
- Реализация:
- Увеличен межсекционный отступ между `Input` и `Output` на +20.
- Каждая строка параметров переведена в 3 зоны: `key | value | copy`.
- Иконка копирования фиксирована в конце строки (правая зона), со вспышкой `✓` после успешного копирования.
- Добавлена функция `copyParamValue` с Clipboard API и fallback через `execCommand`.
- Выполнен version bump: .naeel/cloud-dashboard:v9.35`.
### Шаг P — Скрытие copy для пустых + фикс дневного режима (v9.36)
- Цель: убрать визуальный шум и исправить неполное переключение в light theme.
- Реализация:
- `renderCopyBtnHtml` не рисует кнопку копирования для пустых значений и `—`.
- Добавлены `body.light` стили для `.stat-card` и `.stat-card .lbl`, чтобы карточки статистики реально становились светлыми.
- Выполнен version bump: .naeel/cloud-dashboard:v9.36`.
### Шаг Q — Light-theme для status-badge (v9.37)
- Цель: убрать темные/кислотные бейджи статусов в дневном режиме.
- Реализация:
- Добавлены `body.light` переопределения для `.s-running`, `.s-suspended`, `.s-deleting`, `.s-deleted`, `.s-pending`, `.s-creating`, `.s-default`.
- Для светлой темы введены мягкие светлые фоны и контрастный текст, плюс тонкая рамка бейджа.
- Выполнен version bump: .naeel/cloud-dashboard:v9.37`.
### Шаг R — Контрастный фон блока деталей/параметров (v9.38)
- Цель: убрать визуальное слияние раскрытой области деталей с основной таблицей.
- Реализация:
- Усилен контейнер `.detail-box`: отдельный фон, рамка, скругление, внутренний отступ от таблицы.
- Добавлен фон для `.detail-row td` и контрастная подложка вкладок `.tabs`.
- Увеличен контраст панелей `.panel-params`, `.panel-history`, `.panel-deps` в dark/light режимах.
- Выполнен version bump: .naeel/cloud-dashboard:v9.38`.
### Шаг S — Полосатые строки (zebra) для читаемости (v9.39)
- Цель: улучшить восприятие строк, чтобы данные не сливались по горизонтали.
- Реализация:
- Добавлена мягкая zebra-разметка в основной таблице (`tbody tr.data-row`) с разным тоном соседних строк.
- Добавлена zebra-разметка в таблице истории (`.hist-table tbody tr`).
- Добавлена лёгкая zebra-подложка для строк параметров (`.p-item`).
- Отдельно настроены оттенки для `dark` и `light` темы.
- Выполнен version bump: .naeel/cloud-dashboard:v9.39`.
### Шаг T — Полная деактивация графа + явная зебра основной таблицы (v9.40)
- Цель: полностью убрать интеракции графа и сделать полосатость в основной таблице заметной.
- Реализация:
- Из header удалена кнопка `Граф зависимостей`.
- Из деталей удалена вкладка `Граф` и панель графа.
- Удалены обработчики клика по глобальному графу/модалке, чтобы граф не открывался вообще.
- Полосатость основной таблицы переведена на классы `row-base/row-alt`, назначаемые в `renderDataRow` по индексу данных (не зависит от `detail-row`).
- Усилен контраст тонов зебры в light/dark.
- Выполнен version bump: .naeel/cloud-dashboard:v9.40`.
### Шаг U — Copy для UUID + жирные имена инстансов (v9.41)
- Цель: ускорить работу оператора в таблице и усилить визуальный приоритет названий.
- Реализация:
- В колонке UUID добавлена иконка копирования `⧉` для каждой строки.
- Кнопка UUID-копирования использует существующую логику `copyParamValue` и не триггерит раскрытие строки (`event.stopPropagation()`).
- Имена инстансов в колонке `Имя` переведены в жирное начертание (`.inst-name`).
- Для `uuid-copy` добавлены отдельные hover/ok стили в dark/light теме.
- Выполнен version bump: .naeel/cloud-dashboard:v9.41`.
### Шаг V — Правильная логика иконки day/night (v9.42)
- Цель: сделать поведение кнопки темы интуитивным (иконка показывает целевое переключение, а не текущий режим).
- Реализация:
- В `applyTheme` изменена логика кнопки:
- при активной дневной теме показывается `🌙` (переключение на ночь),
- при активной ночной теме показывается `☀` (переключение на день).
- Обновлены `title`-подсказки на действие: `Переключить на ... тему`.
- Выполнен version bump: naeel/cloud-dashboard:v9.42.
## 15. HANDOFF для нового чата (что делать дальше без доп. подсказок)
Текущее состояние (baseline):
- Таблица загружается через `/instances/stream` один раз на refresh, далее работает из памяти.
- Повторных API-вызовов между refresh нет.
- После reload данные восстанавливаются из browser snapshot (`version=2`, `schema=instance-stream-v2`).
- Удаляемость берется только из backend `_meta.deleteCandidate`.
- В кластере задеплоен образ `naeel/cloud-dashboard:v9.43`.
Нельзя нарушать (инварианты):
- Не добавлять параллельные обращения к внешнему API.
- Не делать догрузку detail по клику строки.
- Не возвращать клиентский fallback вычисления удаляемости.
- Все проектные команды выполнять только через SSH на ВМ.
При старте нового чата сделать в таком порядке:
1. Проверить, что в `k8s/deployment.yaml` увеличен image tag относительно предыдущего.
2. Проверить соответствие stream-контракта: `_progress`, `_detail`, `_meta`, `_done`.
3. Проверить, что `restoreSnapshot()` вызывается до `loadAllDetails()`.
4. Прогнать smoke-flow вручную: login -> refresh -> reload -> refresh.
5. Зафиксировать результат в этот файл отдельным шагом (H, I, ...).
Примечание по sync-проверке путей:
- Путь `/home/naeel/remote_dev/dashboard` может отсутствовать на ВМ, поэтому прямое сравнение хешей с `~/terra/dashboard` не всегда выполнимо на стороне ВМ.
- Если путь отсутствует, источником истины считать `~/terra/dashboard`, а для альтернативной проверки синхронизации запросить у оператора допустимый способ сравнения.
Чеклист smoke-flow (обязательный):
- После login/refresh видно `Загрузка X/Y`.
- После завершения видно `Актуально на ... (загрузка N c)`.
- После reload видно `Актуально на ... (из локального snapshot)`.
- До кнопки `Обновить` новых обращений к API нет.
- Индикатор удаляемости совпадает с backend `_meta.deleteCandidate`.
Definition of Done для следующего изменения:
- Выполнен version bump (минимум patch) в image tag.
- Изменение не ломает table-first режим и snapshot-режим.
- Документация обновлена новым шагом в разделе 14 или отдельной секцией.
### Шаг W — Реальный фикс theme-toggle после валидации pod (v9.43)
- Цель: гарантировать, что в рантайме действительно применяется инвертированная UX-логика иконки day/night.
- Причина: после релиза v9.42 в pod обнаружилась старая логика `applyTheme`.
- Реализация:
- На VM подтвержденно обновлен `static/index.html`:
- `light -> 🌙` (переключение на ночь),
- `dark -> ☀` (переключение на день),
- `title` показывает действие переключения.
- Выполнен повторный version bump: `naeel/cloud-dashboard:v9.43`.
+47
View File
@@ -0,0 +1,47 @@
Контекст:
Работаем с dashboard (FastAPI + static index.html), режим строго практичный: сначала таблица, граф потом.
Модель данных: один полный refresh из API, дальше работа только из памяти; к API повторно ходим только по кнопке Обновить.
Параллельные вызовы к API не используем.
Что уже сделано:
1. Исправлены runtime-регрессии фронта:
- renderDataRow is not defined
- instanceName is not defined
2. Исправлена пагинация backend:
- раньше обрезалось на 99/100
- теперь fetch_all_instances останавливается по пустой странице, а не по size < limit.
3. Метаданные удаляемости:
- удаляемость только для suspended и только если старше 14 дней
- фронт только отображает backend-решение.
4. Визуальные правки:
- сужен индикаторный столбец удаляемости
- вкладки и панели сделаны контрастнее, но без кислотных цветов.
5. Последний релиз:
- image: naeel/cloud-dashboard:v9.28
- rollout в namespace terra успешный
- в рантайме проверено, что новые CSS-правила реально отдаются.
Ключевая последняя правка (v9.28):
- перенос в Output (out) теперь только при реальном переполнении, а не заранее:
- убран агрессивный word-break: break-all
- применены word-break: normal и overflow-wrap: break-word
- расширена grid-раскладка params/out, снято искусственное max-width-ограничение.
Операционные правила:
1. Все команды проекта только по SSH на VM (git, docker, kubectl, curl, проверки).
2. После каждого изменения кода обязательно bump версии image.
3. После деплоя обязательна runtime-проверка в pod.
4. Документацию handoff обновлять сразу после релиза.
Что проверить первым делом в новом чате:
1. UX блока Input/Output на широком экране (нет раннего переноса).
2. Исключить кэш браузера (hard reload), если визуально кажется, что изменений нет.
3. Что в кластере запущен актуальный тег (baseline: v9.28).
Текущий baseline:
- dashboard: v9.28
- состояние: стабильно, критичных открытых багов не зафиксировано.
+56
View File
@@ -0,0 +1,56 @@
# NEW CHAT SUMMARY — 2026-04-17 (baseline v9.42)
## 1) Текущий фокус продукта
- Dashboard для операторов: table-first UX.
- Данные читаются из API только по кнопке `Обновить`.
- Внутри сессии/до refresh работа идет из in-memory/snapshot, без фоновых параллельных API-дозапросов.
## 2) Backend/Frontend контракт
- Источник: `/instances/stream`.
- Фронт ожидает NDJSON-события: `_progress`, `_detail`, `_meta`, `_done`.
- Детали по строке не догружаются отдельным API-вызовом.
## 3) Критичные UX-инварианты (не ломать)
- Сначала таблица, затем детали (параметры/история/зависимости).
- Удаляемость: только backend-логика (`_meta.deleteCandidate`), правило `suspended >= 14 дней`.
- Зеленая точка в колонке `Изменён` только для delete-candidate.
- В light-theme статусы (`running/suspended/...`) имеют отдельные светлые палитры.
- Граф зависимостей полностью отключен в UI (кнопки/вкладки/обработчики удалены).
## 4) Что изменено в этой сессии (ключевое)
- Усилен контраст области деталей относительно основной таблицы.
- Добавлена/усилена zebra-полосатость:
- основная таблица (`row-base/row-alt`),
- история,
- параметры.
- В таблице:
- имена инстансов сделаны жирными,
- в UUID добавлена кнопка копирования `⧉` с `stopPropagation`.
- Исправлена логика кнопки темы:
- если сейчас ночь -> иконка `☀` (переключить на день),
- если сейчас день -> иконка `🌙` (переключить на ночь).
## 5) Актуальная версия и деплой
- Docker image: `naeel/cloud-dashboard:v9.42`.
- `k8s/deployment.yaml` обновлен до `v9.42`.
- Rollout выполнен успешно.
## 6) Важные файлы
- `static/index.html` — вся клиентская логика/рендер/темы/таблица.
- `main.py` — backend API и stream endpoint.
- `k8s/deployment.yaml` — текущий image tag.
- `doc/CHAT_CONTEXT_FULL.md` — пошаговый журнал изменений (A..V).
## 7) Что проверить первым делом в новом чате
1. Убедиться, что в кластере и в deployment реально `v9.42`.
2. Smoke UX:
- login -> refresh -> видна прогресс-загрузка,
- строки таблицы полосатые,
- графа нигде нет,
- UUID копируется без раскрытия строки,
- кнопка темы показывает целевое действие (☀/🌙).
3. Проверить, что нет API-запросов между refresh (кроме явных пользовательских действий).
## 8) Операционная дисциплина
- Команды по проекту выполнять через SSH на VM.
- Перед каждым деплоем обязательно increment image tag (минимум patch).
+56
View File
@@ -0,0 +1,56 @@
# NEW CHAT SUMMARY — 2026-04-17 (baseline v9.43)
## 1) Текущий фокус продукта
- Dashboard для операторов: table-first UX.
- Данные читаются из API только по кнопке `Обновить`.
- Внутри сессии/до refresh работа идет из in-memory/snapshot, без фоновых параллельных API-дозапросов.
## 2) Backend/Frontend контракт
- Источник: `/instances/stream`.
- Фронт ожидает NDJSON-события: `_progress`, `_detail`, `_meta`, `_done`.
- Детали по строке не догружаются отдельным API-вызовом.
## 3) Критичные UX-инварианты (не ломать)
- Сначала таблица, затем детали (параметры/история/зависимости).
- Удаляемость: только backend-логика (`_meta.deleteCandidate`), правило `suspended >= 14 дней`.
- Зеленая точка в колонке `Изменён` только для delete-candidate.
- В light-theme статусы (`running/suspended/...`) имеют отдельные светлые палитры.
- Граф зависимостей полностью отключен в UI (кнопки/вкладки/обработчики удалены).
## 4) Что изменено в этой сессии (ключевое)
- Усилен контраст области деталей относительно основной таблицы.
- Добавлена/усилена zebra-полосатость:
- основная таблица (`row-base/row-alt`),
- история,
- параметры.
- В таблице:
- имена инстансов сделаны жирными,
- в UUID добавлена кнопка копирования `⧉` с `stopPropagation`.
- Исправлена логика кнопки темы:
- если сейчас ночь -> иконка `☀` (переключить на день),
- если сейчас день -> иконка `🌙` (переключить на ночь).
## 5) Актуальная версия и деплой
- Docker image: `naeel/cloud-dashboard:v9.43`.
- k8s/deployment.yaml обновлен до v9.43.
- Rollout выполнен успешно.
## 6) Важные файлы
- `static/index.html` — вся клиентская логика/рендер/темы/таблица.
- `main.py` — backend API и stream endpoint.
- `k8s/deployment.yaml` — текущий image tag.
- `doc/CHAT_CONTEXT_FULL.md` — пошаговый журнал изменений (A..W).
## 7) Что проверить первым делом в новом чате
1. Убедиться, что в кластере и в deployment реально v9.43.
2. Smoke UX:
- login -> refresh -> видна прогресс-загрузка,
- строки таблицы полосатые,
- графа нигде нет,
- UUID копируется без раскрытия строки,
- кнопка темы показывает целевое действие (☀/🌙).
3. Проверить, что нет API-запросов между refresh (кроме явных пользовательских действий).
## 8) Операционная дисциплина
- Команды по проекту выполнять через SSH на VM.
- Перед каждым деплоем обязательно increment image tag (минимум patch).