Тема
ТЗ — Флит-дашборд (студийный сервис) (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_sites | domain, channel, last_seen_at, core_version, modules_versions (json) | Карта парка, обновляется телеметрией |
fleet_backup_status | site_id, last_backup_at, backup_size, status | Контроль свежести бэкапов |
fleet_update_waves | id, package, target_version, percent, status, started_at | Канареечные волны обновлений |
fleet_wave_sites | wave_id, site_id, status, health_result (json) | Результат волны по каждому сайту |
fleet_availability_log | site_id, check_source, status, checked_at | Журнал внешних проверок доступности |
modules_versions/health_result — JSON-столбцы с cast array; channel/status — enum → PHP Enum. FK site_id/wave_id — constrained()->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/waves | package, target_version, percent | permission fleet-dashboard.manage, FormRequest: percent 1–100, package — известный пакет из карты парка |
Filament: остановка волны / POST .../waves/{id}/stop | wave_id, опционально reason | permission 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/meta | JSON 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_hours | int | 48 | — | Порог, после которого бэкап сайта считается устаревшим |
fleet-dashboard.wave_error_rate_threshold | float | 0.02 | — | Порог error-rate (Sentry) для остановки волны |
fleet-dashboard.telemetry_signature_ttl_minutes | int | 10 | — | Допустимое расхождение времени подписи телеметрии |
fleet-dashboard.availability_check_interval_minutes | int | 5 | — | Периодичность внешней проверки доступности сайтов парка |
fleet-dashboard.availability_log_retention_days | int | 30 | — | Ретеншн fleet_availability_log — без него журнал растёт неограниченно |
fleet-dashboard.availability_flap_debounce_checks | int | 3 | — | Число подряд неудачных внешних проверок до алерта (защита от флаппинга) |
fleet-dashboard.max_wave_step_percent | int | 25 | — | Максимальный шаг процента волны за один старт/продвижение без явного подтверждения |
fleet-dashboard.emergency_stop_all_waves | action/флаг | off | — | Kill-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/sites | fleet-dashboard.manage | Карта парка (версии, канал, health, бэкапы; keyset-пагинация) |
| POST | /api/v1/admin/fleet-dashboard/waves | fleet-dashboard.manage | Старт канареечной волны обновления |
| GET | /api/v1/admin/fleet-dashboard/waves/{id} | fleet-dashboard.manage | Статус волны по сайтам |
| POST | /api/v1/admin/fleet-dashboard/waves/{id}/stop | fleet-dashboard.manage | Ручная остановка волны |
| POST | /api/v1/admin/fleet-dashboard/waves/emergency-stop | fleet-dashboard.manage | Kill-switch: остановка ВСЕХ активных волн разом |
| GET | /api/v1/admin/fleet-dashboard/backups/at-risk | fleet-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-rate | wave_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) | in | Error-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; не упал ли scheduler | cms: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запрещены.