Skip to content

ТЗ — Флит-дашборд (студийный сервис) (cms/fleet-dashboard)

Слой: 🔵 инфра-модуль · Зрелость доноров: ★ · Донор: proxy (proxy dns watch — референс мониторинга) Статус: ТЗ к разработке

Назначение и возможности

Особенность: это не модуль клиентского сайта, а отдельный сервис на инфраструктуре студии. Он принимает телеметрию от cms/updates со всех сайтов управляемого парка (версии, канал, health-статус, бэкапы), ведёт канареечные волны обновлений и контролирует состояние бэкапов парка. Полное описание процесса и модели версий — /cms-v2/update-center. Референс мониторинга внешней доступности — proxy dns watch/proxy dns errors из инфраструктуры reverse-proxy студии (внешние точки проверки, не с самого сервера — урок VPN-петли).

  • Приём ежесуточной телеметрии от cms/updates каждого сайта (подписанный POST);
  • карта парка: какой сайт на какой версии ядра/модулей/канале, дата последнего бэкапа;
  • управление канареечными волнами: старт волны, процент сайтов, гейт по error-rate (Sentry);
  • автоматическая остановка волны при провале health-check у канареечного сайта;
  • контроль бэкапов парка: сайты без свежего бэкапа дольше порога — в списке риска;
  • мониторинг внешней доступности сайтов парка (внешние резолверы/точки, не с самого сервера);
  • операционная база для отчёта клиенту по подписке на поддержку (генерируется отсюда).

Зависимости и выключение

requires: — (сервис студии, не клиентский модуль; клиентские сайты зависят от него через cms/updates) · provides: fleet-monitoring

Поведение при выключении: клиентские сайты продолжают работать автономно, но студия теряет централизованную видимость парка — волны обновлений и контроль бэкапов нужно вести вручную по каждому сайту.

Модель данных

ТаблицаКлючевые поляПримечание
fleet_sitesdomain, channel, last_seen_at, core_version, modules_versions (json)Карта парка, обновляется телеметрией
fleet_backup_statussite_id, last_backup_at, backup_size, statusКонтроль свежести бэкапов
fleet_update_wavesid, package, target_version, percent, status, started_atКанареечные волны обновлений
fleet_wave_siteswave_id, site_id, status, health_result (json)Результат волны по каждому сайту
fleet_availability_logsite_id, check_source, status, checked_atЖурнал внешних проверок доступности

modules_versions/health_result — JSON-столбцы с cast array; channel/status — enum → PHP Enum. FK site_id/wave_idconstrained()->index() во всех дочерних таблицах. fleet_availability_log — журнальная append-only таблица высокого объёма (проверки каждые availability_check_interval_minutes на весь парк) — кандидат на BRIN по checked_at.

ПДн-паспорт: таблицы модуля не хранят персональных данных — только операционные метаданные парка сайтов (домен, версии, канал, статус здоровья, даты бэкапов). Данные посетителей и пользователей клиентских сайтов сюда не попадают и не проходят через телеметрию. Участие в «выгрузить всё по субъекту»/«забыть по запросу» (152-ФЗ) — не применимо: субъекта ПДн в этих таблицах нет.

Входные и выходные данные

Входы

ИсточникДанные/поляЧем валидируется
POST /api/v1/fleet-dashboard/telemetry (от cms/updates каждого сайта)site_id/домен, core_version, modules_versions, channel, health_status, дата бэкапаподписанный запрос + токен сайта, TTL расхождения времени (telemetry_signature_ttl_minutes); схема payload валидируется до записи
Filament: старт волны / POST /api/v1/admin/fleet-dashboard/wavespackage, target_version, percentpermission fleet-dashboard.manage, FormRequest: percent 1–100, package — известный пакет из карты парка
Filament: остановка волны / POST .../waves/{id}/stopwave_id, опционально reasonpermission fleet-dashboard.manage
Sentry (error-rate по релизу, через API)частота ошибок до/послевнешний вызов с таймаутом при гейте волны
Внешние точки проверки доступностистатус ответа сайта паркане с самого сервера студии (VPN разрывает петлю на свой IP)

Состав телеметрии (контракт с клиентом)

Полный состав полей, которые ежесуточно уходят с клиентского сайта на fleet-dashboard через cms/updates (см. также таблицу входов выше):

ПолеЗначение
domainДомен сайта — идентификатор в карте парка
core_versionВерсия ядра CMS
modules_versionsВерсии установленных модулей (json)
channelКанал обновлений (stable/beta/…)
health_statusРезультат self-test/health-чека сайта
last_backup_atДата последнего успешного бэкапа

Это операционные метаданные версий и здоровья инсталляции — не содержимое сайта клиента (страницы, медиа, заказы, обращения) и не персональные данные посетителей сайта. Состав телеметрии — часть условий managed-подписки: клиент информируется о нём в договоре/оферте студии, это не скрытый сбор данных; приведённый список — контракт, который команда обязана держать актуальным при добавлении новых полей телеметрии (изменение состава — правка ТЗ и оферты, не тихий выпуск).

Выходы

ПотребительДанныеФормат
Filament UI студиикарта парка, статус волн, риск по бэкапамBlade через сервис модуля
/api/v1/admin/fleet-dashboard/*конверт data/metaJSON REST, keyset-пагинация
cms/updates каждого сайта (через API волны)сигнал go/halt по волнеподписанный ответ на входящий опрос/POST
fleet_sites/fleet_backup_status/fleet_update_waves/fleet_wave_sites/fleet_availability_logкарта парка, история волн и доступностиБД

Настройки (группа fleet-dashboard)

КлючТипДефолтaffectsPageCacheОписание
fleet-dashboard.backup_staleness_hoursint48Порог, после которого бэкап сайта считается устаревшим
fleet-dashboard.wave_error_rate_thresholdfloat0.02Порог error-rate (Sentry) для остановки волны
fleet-dashboard.telemetry_signature_ttl_minutesint10Допустимое расхождение времени подписи телеметрии
fleet-dashboard.availability_check_interval_minutesint5Периодичность внешней проверки доступности сайтов парка
fleet-dashboard.availability_log_retention_daysint30Ретеншн fleet_availability_log — без него журнал растёт неограниченно
fleet-dashboard.availability_flap_debounce_checksint3Число подряд неудачных внешних проверок до алерта (защита от флаппинга)
fleet-dashboard.max_wave_step_percentint25Максимальный шаг процента волны за один старт/продвижение без явного подтверждения
fleet-dashboard.emergency_stop_all_wavesaction/флагoffKill-switch: аварийная остановка ВСЕХ активных волн разом (не обычная остановка одной волны через .../waves/{id}/stop) — на случай массового инцидента на парке

Лимиты и квоты матрицы v2.2 закрыты max_wave_step_percent (защита от резкого выката на весь парк за один шаг) и availability_log_retention_days (защита от неограниченного роста журнала доступности) — оба со значением по умолчанию, не оставлены на усмотрение оператора без дефолта. Kill-switch emergency_stop_all_waves доступен только под fleet-dashboard.manage, останавливает разом все активные волны независимо от их индивидуального гейта и требует явного подтверждения в UI (см. «UX-требования»).

API

МетодПутьДоступНазначение
POST/api/v1/fleet-dashboard/telemetryподписанный запрос (токен сайта)Приём суточной телеметрии от cms/updates
GET/api/v1/admin/fleet-dashboard/sitesfleet-dashboard.manageКарта парка (версии, канал, health, бэкапы; keyset-пагинация)
POST/api/v1/admin/fleet-dashboard/wavesfleet-dashboard.manageСтарт канареечной волны обновления
GET/api/v1/admin/fleet-dashboard/waves/{id}fleet-dashboard.manageСтатус волны по сайтам
POST/api/v1/admin/fleet-dashboard/waves/{id}/stopfleet-dashboard.manageРучная остановка волны
POST/api/v1/admin/fleet-dashboard/waves/emergency-stopfleet-dashboard.manageKill-switch: остановка ВСЕХ активных волн разом
GET/api/v1/admin/fleet-dashboard/backups/at-riskfleet-dashboard.manageСписок сайтов с устаревшим бэкапом (keyset-пагинация)

Компоненты

Filament: дашборд парка (таблица сайтов с версиями/каналом/статусом здоровья), страница волн обновления (прогресс, гейт по error-rate, кнопка остановки), виджет «бэкапы в риске». Команды: cms:fleet-dashboard:wave:advance --json (продвижение волны на следующий процент при зелёном гейте), cms:fleet-dashboard:doctor --json.

Демо-сидеры демо-контента: не применимо — это студийный внутренний сервис без блоков/виджетов клиентского сайта, галереи блоков и playground для него нет.

События и обмен

СобытиеКогдаPayload
FleetTelemetryReceivedПринята телеметрия от сайта паркаsite_id, core_version, channel
FleetWaveStartedЗапущена канареечная волнаwave_id, package, target_version, percent
FleetWaveHaltedВолна остановлена по гейту error-ratewave_id, reason
FleetBackupStaleБэкап сайта устарел дольше порогаsite_id, last_backup_at

Реализует контракт fleet-monitoring (сервис студии, а не устанавливаемый на клиентском сайте модуль). Слушает: телеметрию, отправляемую событием TelemetrySent модуля cms/updates на клиентских сайтах (принимается как внешний подписанный POST, не как внутреннее событие одного процесса — сервис живёт на отдельной инфраструктуре). FilterBus и provides-контракты в клиентском смысле не применимы — единственный потребитель контракта fleet-monitoring — сама студия.

Таблица взаимодействий

Сущность/модульКаналНаправлениеЧто происходит
cms/updates (все сайты парка)внешний подписанный POST (вне пяти каналов — межпроцессный, не внутримодульный)inСуточная телеметрия версий/канала/бэкапа/health
cms/updates (сайт в волне)внешний API, подписанный запросoutСигнал go/halt по канареечной волне
Sentry студии/сайтоввнешняя интеграция (HTTPS)inError-rate по релизу для гейта волны
Внешние точки проверки доступностивнешняя интеграцияinСтатус ответа сайтов парка (не с самого сервера)
cms/backup на каждом сайте (косвенно, через телеметрию)данные внутри чужого payload, не прямая связьinДата последнего бэкапа для контроля свежести

Фоновая работа

Очередь fleet-dashboard: обработка входящей телеметрии, продвижение канареечной волны (wave:advance) и проверки внешней доступности парка — все идемпотентны (повторная телеметрия с тем же site_id/датой не дублирует запись, повторная проверка волны не продвигает её дважды за один гейт). Расписание внешних проверок доступности (availability_check_interval_minutes) — через ScheduleRegistrar ядра; проверки идут с внешних точек (не с самого сервера студии — VPN разрывает петлю на собственный IP).

Метрики и алерты

Метрики модуля (видны в Pulse/health): доля сайтов парка без свежей телеметрии (last_seen_at дольше суток), длительность волны от старта до 100% (по волнам за последние 30 дней), error-rate по каждой активной волне в реальном времени.

Алерты: FleetBackupStale (бэкап сайта устарел дольше backup_staleness_hours), флаппинг доступности сайта парка (сработал дебаунс availability_flap_debounce_checks), автоматическая остановка волны (FleetWaveHalted — гейт error-rate сработал, требуется решение оператора).

Симптом → диагностика → команда

СимптомЧто проверитьКоманда/действие
Сайт не шлёт телеметрию N днейfleet_sites.last_seen_at; статус cms/updates на самом сайте (очередь, cron)cms:fleet-dashboard:doctor --json — список сайтов без свежей телеметрии
Волна зависла на проценте (не продвигается)fleet_update_waves.status; error-rate волны в Sentry; не упал ли schedulercms:fleet-dashboard:wave:advance --json --dry-run — покажет причину, почему гейт не пропускает
Алерт-шторм по доступности (много сайтов «недоступны» разом)все точки проверки разом, а не один сайт → сбой мониторинга, не паркасверить fleet_availability_log за окно инцидента; availability_flap_debounce_checks вручную не поднимать без осознанного решения — временная мера, не тихая правка настройки
Гонка двух операторов на одну волнувторой старт волны на тот же пакет+канал отклонён с id уже идущей волныGET /api/v1/admin/fleet-dashboard/waves/{id} — кто и когда стартовал существующую волну

Бэкап и рестор

В бэкап попадают fleet_sites, fleet_backup_status, fleet_update_waves, fleet_wave_sites — состояние карты парка и история волн, без них рестор теряет операционную историю студии. fleet_availability_log — журнал высокого объёма, не критичен для восстановления состояния парка: допустимо исключить из бэкапа или держать коротким ретеншном (тем же порядком, что и availability_log_retention_days). После рестора карта парка (last_seen_at, версии, health) не требует ручного восстановления — досинхронизируется автоматически следующей суточной телеметрией от каждого сайта.

Производительность и кеш

Объёмы: студийный парк — десятки-сотни клиентских сайтов, телеметрия раз в сутки на сайт → сотни записей/сутки в fleet_sites (upsert, не insert-only); fleet_availability_log — журнал высокого объёма (проверки каждые availability_check_interval_minutes на весь парк, при 5 минутах и 100 сайтах это ~28800 строк/сутки) — BRIN по checked_at обязателен, ретеншн нужен отдельной настройкой (сейчас не объявлен — добавлен ниже). Горячий путь — рендер карты парка в Filament: агрегация по всем сайтам одним проходом (with()/withCount(), не запрос на сайт в цикле). Тег fleet-dashboard.map — агрегированная карта парка кешируется для быстрого рендера дашборда; инвалидация — событием FleetTelemetryReceived. Page-cache клиентских сайтов модуль не затрагивает — это внутренний инструмент студии, публичный трафик клиентских сайтов от него не зависит.

Безопасность

Входные границы: /api/v1/fleet-dashboard/telemetry — единственный внешний вход, принимает только подписанный запрос с проверкой TTL расхождения времени подписи (telemetry_signature_ttl_minutes); телеметрия без валидной подписи или с истёкшим TTL отклоняется. Управление волнами и просмотр карты парка — строго под fleet-dashboard.manage, доступ только сотрудникам студии, сервис не выдаётся клиентам ни в каком виде. Волна обновления останавливается автоматически при превышении порога error-rate — защита от массового выката сломанной версии на парк. Специфичные векторы: подмена site_id/токена в POST телеметрии (сайт присваивает себе чужую историю версий) — токен привязан к домену на этапе выдачи, не передаётся в теле запроса как произвольное значение; replay атака на телеметрию — TTL-окно отклоняет старые подписанные запросы; злоупотребление стартом волны на весь парк разом (percent=100 без промежуточных ступеней) — минимальный шаг волны стоит зафиксировать настройкой, не оставлять на усмотрение оператора без подсказки в UI.

Матрица ролей

Рольfleet-dashboard.view (карта парка, волны, риски бэкапов)fleet-dashboard.manage (старт/остановка волн, emergency stop)
Сотрудник студии — оператор поддержки
Сотрудник студии — инженер/тимлид
Админ клиентского сайта (любого)✖ нет доступа✖ нет доступа
Менеджер клиентского сайта (любого)✖ нет доступа✖ нет доступа
Редактор клиентского сайта (любого)✖ нет доступа✖ нет доступа

Клиентские роли любого сайта парка не имеют доступа к fleet-dashboard вовсе — это студийный сервис, а не клиентский модуль, permissions fleet-dashboard.* не выдаются гвардам клиентских Filament-панелей и не отображаются в их UI ни в каком виде (не просто скрытая кнопка — сам роут недоступен).

UX-требования

Студийный оператор (единственная аудитория — админ этого сервиса): пустое состояние «сайты ещё не присылали телеметрию» с подсказкой ожидаемого времени первой синхронизации; массовое действие — старт волны сразу на группу сайтов по каналу/версии, не по одному; ошибки на человеческом языке («сайт example.ru не отвечает 3 часа», не код статуса); подтверждение перед стартом волны — модальное окно с числом затрагиваемых сайтов и процентом, отдельное подтверждение при percent выше разумного порога за один шаг; список «бэкапы в риске» сортирован по давности, не по алфавиту — самое срочное сверху; волна, остановленная автоматически по гейту, визуально отличается от остановленной вручную (разный цвет/иконка, не только текст в логе).

Крайние случаи и типовые баги

  • рассинхрон версий парка (сайт не присылал телеметрию дольше ожидаемого — процесс упал, cms/updates выключен) → карта парка помечает сайт «нет данных N дней», не показывает последнюю известную версию как актуальную без пометки давности;
  • лицензия/подписка на поддержку истекла посреди волны (для fleet-dashboard — аналог: сайт вышел из managed-парка во время активной волны) → волна на этом сайте останавливается по этому конкретному fleet_wave_sites, остальные сайты волны не затрагиваются;
  • гонка: два оператора студии стартуют волну на один и тот же пакет одновременно → лок на уровне fleet_update_waves (один активный wave на пакет+канал), второй старт отклоняется с понятным сообщением «волна уже идёт, id …»;
  • чек-флаппинг доступности (внешняя точка то видит сайт, то нет из-за сетевого шума) → fleet_availability_log не триггерит алерт на единичный сбой — нужен дебаунс по аналогии с cms/health (число подряд неудачных проверок), иначе алерт-шторм по всему парку при кратковременном сетевом инциденте;
  • идемпотентность повторной телеметрии — тот же site_id и та же дата присланы дважды (retry клиента при таймауте ответа) → upsert по (site_id, date), не дублирующая строка в fleet_sites;
  • выключение fleet-dashboard посреди активной волны → студия теряет централизованное управление, но клиентские cms/updates продолжают штатно работать автономно — важно: активная волна не может быть корректно продолжена без сервиса, поэтому выключение с активной волной должно требовать явного подтверждения «волна X будет приостановлена»;
  • сбой/таймаут внешнего API Sentry при гейте волны → таймаут обязателен; при недоступности Sentry волна не продвигается автоматически (fail-safe — не «пропустить проверку и продолжить», а «остановиться и ждать ручного решения»); Sentry студии — self-hosted или тарифный план с квотой запросов/событий, расход виден в самом Sentry (fleet-dashboard не дублирует счётчик), исчерпание квоты обрабатывается тем же fail-safe путём, что и недоступность API;
  • пустой парк (студия только настроила сервис, сайтов ещё нет) → дашборд явно говорит «сайтов пока нет» вместо пустой таблицы, не создаёт ощущение поломки;
  • огромный парк (сотни сайтов, гипотетический рост) → карта строится агрегацией без N+1, список «бэкапы в риске» — keyset-пагинация уже заложена в API;
  • измерения locale/city/site — на уровне fleet-dashboard измерение то же самое, что и site_id из карты парка (не locale/city внутри одного сайта клиента) — путаница терминов: fleet_sites.site_id-аналог здесь означает «клиентский сайт целиком», не измерение мультисайта одной инсталляции;
  • противоречивая комбинация настроек: wave_error_rate_threshold установлен ниже естественного base rate ошибок парка (волна останавливается почти сразу на любом релизе) → нужна подсказка в UI при сохранении настройки, если порог заметно ниже исторического среднего error-rate парка, иначе волны никогда не доезжают до 100%.

Донорский код

Что взятьПуть
Паттерн внешнего мониторинга доступности (не с самого сервера, ретраи против VPN-флапов)servers/proxy-nix — соседний с laravel/ каталог (/home/aleksey/projects/servers/proxy-nix): cli/watchdog.sh + systemd-таймер proxy-watchdog.timer, proxy dns watch/proxy dns errors

Разграничение: proxy dns watch — донор паттерна кода мониторинга (как опрашивать внешние точки, не с самого сервера), а не донор данных. У fleet-dashboard нет предшествующей legacy-системы и боевых данных для переноса — это новая разработка с нуля. Легаси-импорт §16 (cms:fleet-dashboard:import-legacy) — не применимо.

Тесты и приёмка

  • [ ] Контрактные тесты: телеметрия без валидной подписи или с истёкшим TTL отклоняется;
  • [ ] health-чек модуля проверяет собственную доступность приёма телеметрии (не сайтов парка);
  • [ ] волна обновления останавливается автоматически при превышении порога error-rate;
  • [ ] деградация при недоступности сервиса — клиентские сайты продолжают работать автономно;
  • [ ] права строго ограничивают доступ сотрудниками студии, не клиентами;
  • [ ] нет N+1 при построении карты парка (агрегация по всем сайтам одним проходом);
  • [ ] проверки внешней доступности идут с внешних точек, не с самого сервера студии;
  • [ ] повторная телеметрия с тем же site_id за тот же день не дублирует запись (upsert-идемпотентность);
  • [ ] две волны на один пакет+канал одновременно не запускаются (лок на уровне БД);
  • [ ] флаппинг внешней проверки доступности не поднимает алерт до availability_flap_debounce_checks;
  • [ ] ретеншн fleet_availability_log применяется автоматически по availability_log_retention_days;
  • [ ] emergency_stop_all_waves останавливает все активные волны разом, требует подтверждения в UI и недоступен без fleet-dashboard.manage;
  • [ ] состав полей телеметрии (домен, core_version, modules_versions, channel, health_status, last_backup_at) зафиксирован контрактным тестом — новое поле без обновления ТЗ и оферты не проходит ревью;
  • [ ] клиентские роли (админ/менеджер/редактор любого сайта) не получают доступ ни к одному эндпоинту fleet-dashboard.* — негативный тест на 403;
  • [ ] контрактный набор cms-testing зелёный, пакет протестирован в testbench-изоляции;
  • [ ] feature-тест на каждый роут API; тестовая БД только fleet-dashboard_test, migrate:fresh/refresh/reset запрещены.

Внутренняя база знаний студии