Files
dashboard/doc/CHAT_CONTEXT_FULL.md

501 lines
35 KiB
Markdown
Raw Permalink 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.
# Dashboard Service - Full Chat Context For Next Agent
Updated: 2026-04-16
Owner: naeel
## 1. Что это за сервис
Cloud Dashboard - отдельный веб-сервис (backend + frontend), который показывает инстансы облака, фильтрацию, детали, историю операций и граф зависимостей.
Текущая реализация:
- Backend: Python + FastAPI
- Frontend: статический SPA (HTML/CSS/vanilla JS)
- Граф: Cytoscape
- Runtime: контейнер в Kubernetes
## 2. Бизнес-цель
Сделать рабочий дашборд для повседневной эксплуатации:
- быстрый вход по API token
- фильтрация по статусам
- понятная таблица и карточки статистики
- просмотр параметров/зависимостей инстанса
- визуализация зависимостей
- стабильная работа в PROD
## 3. Ключевые сделанные фичи
### 3.1 Аутентификация и сессия
- Логин по API token.
- Токен сохраняется в localStorage.
- Logout очищает токен.
### 3.2 Выбор стенда (ВАЖНО)
Сделано переключение окружений на экране логина:
- PROD -> https://deck-api.ngcloud.ru/api/v1
- DEV -> https://deck-api-dev.ngcloud.ru/api/v1
- TEST -> https://deck-api-test.ngcloud.ru/api/v1
Как работает:
- выбранный стенд сохраняется в localStorage как deck_env
- frontend отправляет в backend заголовок X-Deck-Env
- backend выбирает base URL по whitelist
- если значение некорректное, fallback на test
### 3.3 Таблица и UX
- Фильтры статусов с чекбоксами (увеличены).
- Sticky header и правильный скролл области таблицы.
- Сортировки по колонкам.
- Статистика по статусам.
- Detail row с вкладками: параметры, история, зависимости, граф.
### 3.4 Зависимости и графы
Добавлены два режима:
- граф для конкретного инстанса
- общий граф для running
Оптимизации:
- lazy-load графа (не считать сразу)
- кэш графа на backend с TTL
- клиентская подвыборка subgraph
- индикаторы загрузки
- open large graph in modal/fullscreen behavior при большом числе узлов
UI графа:
- прямоугольные узлы
- более крупный и читаемый текст
- улучшенные стрелки/контраст
- упрощенный layout (grid-подход)
### 3.5 Визуал и брендинг
- day/night переключатель темы с сохранением в localStorage
- favicon/logo из docs assets
- удалены лишние декоративные элементы
## 4. Техническая архитектура
## Browser
- SPA рендерит UI
- вызывает backend по префиксу /dashboard/api
- хранит deck_token и deck_env в localStorage
## Backend (FastAPI)
Главные маршруты:
- GET /api/instances
- GET /api/instances/{uid}
- GET /api/graph
Транспорт:
- принимает X-Deck-Token
- принимает X-Deck-Env
- проксирует запросы в Deck API выбранного окружения
Граф:
- собирается из списка инстансов и деталей
- edges из dependencies/dependentInstances
- кэш in-memory по ключу token+env+statuses
## Kubernetes
- deployment/service/ingress в namespace terra
- публичный доступ через /dashboard
## 5. Эксплуатационные детали
- Сервис не использует БД.
- Основная интеграция backend - только с Deck API.
- Узкое место в производительности обычно внешний API, не сам UI.
- В проекте уже обсужден путь: сначала стабилизировать логику на Python, потом при необходимости переписать backend на Go с сохранением API контрактов.
## 6. История решений (сжатая)
1) Сначала починили критичные JS ошибки и нестабильный логин.
2) Исправили фильтры статусов и прокинули корректный parsing на backend.
3) Переделали скролл + sticky элементы.
4) Добавили графы зависимостей, затем несколько итераций UX/производительности.
5) Сделали lazy загрузку и кэш.
6) Подчистили визуал (читаемость, стрелки, тема, favicon/logo).
7) Реализовали выбор стенда PROD/DEV/TEST на логине с end-to-end маршрутизацией.
8) Вынесли сервис в новую репу-папку dashboard и добавили правила агента + gitignore.
## 7. Что уже перенесено в новую репу dashboard
Из старого пути cloud-dashboard в новый dashboard перенесены:
- Dockerfile
- main.py
- requirements.txt
- static/
- k8s/
Дополнительно создано:
- .github/copilot-instructions.md (скопированы правила агента)
- .gitignore (Python/кэши/локальные артефакты)
## 8. Что важно для нового агента
1) Не ломать контракт frontend/backend без явного запроса.
2) Заголовки X-Deck-Token и X-Deck-Env обязательны.
3) Поддерживать 3 стенда через whitelist.
4) Не тащить секреты/токены в git.
5) Для графа сохранять lazy-load и кэш (иначе будет тяжелая загрузка).
## 9. Рекомендованный план ближайших шагов
1) Сделать первичный commit в новой репе dashboard.
2) Настроить CI:
- lint
- build image
- push image
- deploy (опционально)
3) Добавить smoke tests:
- /api/instances с разными env
- /api/graph с фильтрами
4) Подготовить migration checklist для возможного будущего Go backend.
## 10. Definition of Done (текущее состояние)
Сервис:
- работает в k8s
- логин и фильтры стабильны
- графы и тема работают
- выбор стенда реализован и сохраняется
- код перенесен в новую папку/repo baseline
## 11. Текущие задачи (v9.7 — в работе)
Все 4 задачи согласованы с оператором. Реализовывать в таком порядке:
### Задача 1 — Стриминговая загрузка инстансов
- Backend `/api/instances/stream` уже есть (NDJSON, main.py строка ~297)
- Фронт **ещё использует старый** `/api/instances` (index.html строка ~1004)
- Нужно: переключить фронт на `/api/instances/stream`, строки добавлять в таблицу по одной по мере прихода
- Все загруженные инстансы держать в памяти (`allInstances`). К API обращаться ТОЛЬКО по кнопке "Обновить"
### Задача 2 — Метка "Актуально на ЧЧ:ММ:СС"
- Разместить **слева от кнопки "Обновить"**
- Показывать время завершения последней загрузки
- Обновлять при каждом refresh
### Задача 3 — Фильтрация зависимостей по чекбоксам
- В блоке "Зависимости" инстанса показывать ТОЛЬКО те инстансы, статус которых выбран чекбоксами (running/suspended/etc)
- При изменении чекбокса — сразу перерисовывать список зависимостей (без обращения к API, из памяти)
### Задача 4 — Переход по клику + кнопка "Назад"
- Клик на инстанс в блоке "Зависимости" → открыть его строку/детали в таблице
- Добавить кнопку "Назад" для возврата к исходному инстансу
### Правила разработки (напоминание):
- ЧАСТО коммитить: `git add -A && git commit` на ВМ в `~/terra/dashboard`
- Всё через SSH: `ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213`
- Не ломать существующий рабочий код
- Новая логика = новые функции в конце файла
---
## 12. Операционная дисциплина (КРИТИЧЕСКОЕ)
Для нового агента это обязательное правило:
- Все команды по проекту выполнять ТОЛЬКО через SSH на ВМ.
SSH-шаблон:
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 КОМАНДА
Локально ЗАПРЕЩЕНО выполнять:
- git-команды проекта
- docker-команды
- kubectl-команды
- curl к API проекта
- bash/python/go/тесты проекта
Почему:
- так исключаются конфликты сред и ложные результаты;
- единая точка выполнения (ВМ) дает предсказуемость и воспроизводимость.
---
## 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`.