Skip to content

ТЗ — Центр обновлений (клиент) (cms/updates)

Слой: 🔵 инфра-модуль, обязательный в managed-парке · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

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

Клиентская часть спеки /cms-v2/update-center: страница «Обновления» в Filament, проверка доступных версий модулей/ядра с приватного Satis студии и запуск пайплайна cms:upgrade (бэкап → dry-run → migrate → health-гейт → откат при провале). Обязателен на всех сайтах managed-парка — без него сайт выпадает из процедуры централизованного обновления и телеметрии.

Граница ядро/модуль (core-criteria §10): реестр модулей, версии (cms_module_versions/cms_module_history), машина состояний, cms:doctor/cms:map — зона ядра; пайплайн cms:upgrade, Satis-клиент, UI обновлений — зона этого модуля.

  • Страница «Обновления»: установлено vs доступно, changelog по каждому пакету;
  • запуск обновления как job (не HTTP-запрос — таймауты php-fpm не позволяют синхронно);
  • полный пайплайн cms:upgrade: бэкап БД, composer install --dry-run, миграции additive-only, down/up с secret-доступом, сброс кешей, queue:restart, smoke-test;
  • автоматический откат всех трёх слоёв (симлинк/composer.lock/схема БД) при провале health-гейта;
  • индикатор «доступно обновление безопасности» по severity в changelog-манифесте;
  • участие в канареечных волнах (канал stable/beta, статус волны от флит-дашборда);
  • ежесуточная телеметрия на флит-дашборд студии (подписанный POST): версии, канал, дата бэкапа, health-статус.

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

requires: cms/backup, cms/health · suggests: cms/notifications-bus (уведомление об успехе/провале обновления) · provides: update-client

Модуль помечен обязательным в managed-парке — отключение блокируется на управляемых установках. На самостоятельных инсталляциях выключение возможно: сайт перестаёт получать обновления и телеметрию, версии проверяются и накатываются вручную через composer/artisan.

Satis студии — внутренняя инфраструктура студии, не тарифицируемый внешний сервис (критерий «Стоимость внешних API» матрицы v2.2 не применим в денежном смысле): нет квоты запросов и биллинга, доступность зависит от инфраструктуры студии, а не от провайдера; недоступность обрабатывается как деградация (см. «Крайние случаи»).

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

Реестр установленных модулей — зона ядра (core-criteria §10): cms_module_versions/cms_module_history определены и принадлежат ядру (core: модель данных), модуль читает их для UI и дописывает историю через сервис ядра на каждом шаге пайплайна — не своей миграцией. Собственная таблица модуля — только журнал запусков:

ТаблицаВладелецКлючевые поляПримечание
cms_module_versionsядроmodule, installed_version, checked_atТекущая версия каждого модуля — читается для страницы «Обновления»
cms_module_historyядроmodule, from_version, to_version, migrations_applied (json), applied_atЖурнал применённых обновлений — модуль дописывает через сервис ядра
cms_update_runsэтот модульid, status, channel, started_at, finished_at, rollback_reasonЖурнал запусков пайплайна cms:upgrade — единственная таблица в собственности модуля

migrations_applied — JSON-столбец с cast array; status/channel в cms_update_runs — enum → PHP Enum. cms_update_runs — журнальная append-only таблица, индекс по started_at, кандидат на BRIN при долгой жизни установки.

ПДн-паспорт: модуль не хранит персональных данных — версии модулей, каналы обновлений, статусы и история запусков пайплайна к субъекту ПДн не относятся; регистрировать обработчик в PrivacyRegistry (cms:privacy:export/forget, п. 15 ревизии ядра) незачем.

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

Входы

ИсточникДанные/поляЧем валидируется
Filament: «Проверить обновления»без полей (триггер)permission updates.manage
Filament / POST /api/v1/admin/updates/runmodules[] (пакеты для обновления), channelFormRequest whitelist: modules.* — только установленный пакет из cms_module_versions, channel — enum stable/beta; неизвестный пакет → 422
Манифест Satis студии (внешний источник)доступные версии, changelog, severityHTTPS + satis_token; схема манифеста валидируется перед разбором, битый JSON — отказ без падения проверки
POST /api/v1/updates/telemetry-ответ волны от cms/fleet-dashboardwave_id, статус гейта (go/halt)подписанный запрос, TTL как у cms/fleet-dashboard
Событие ModuleEnabled/ModuleDisabled (ядро, канал 1)module, versionвнутреннее событие, не пользовательский ввод — только консистентность payload

Выходы

ПотребительДанныеФормат
Filament UIустановлено/доступно, changelog, статус запуска, историяBlade/Livewire через сервис модуля (не запросы из шаблона)
/api/v1/admin/updates/*конверт data/metaJSON REST, keyset-пагинация истории
cms/fleet-dashboardверсии, канал, дата бэкапа, health-статусподписанный POST JSON (суточная телеметрия)
cms/notifications-bus (suggests)факт успеха/провала обновлениясервис-вызов канала 3 (notification-channel), при выключении — log-fallback
cms_update_runs/cms_module_historyстатус пайплайна, применённые миграцииБД, append-only, источник для точного отката

Настройки (группа updates)

КлючТипДефолтaffectsPageCacheОписание
updates.channelstring"stable"Канал обновлений (stable/beta)
updates.auto_check_interval_hoursint24Периодичность проверки доступных версий
updates.telemetry_enabledbooltrueОтправка телеметрии на флит-дашборд; в managed-парке форсируется true на уровне кода, клиент изменить не может (см. крайний случай ниже)
updates.smoke_test_timeout_secondsint60Таймаут health-гейта после применения обновления
updates.maintenance_mode_enabledbooltrueПоказывать страницу «идёт обновление» на время миграций вместо риска 500
updates.notify_onarray["success","failure"]На какие исходы пайплайна отправлять уведомление через notifications-bus
updates.max_concurrent_runsint1Лимит одновременных запусков cms:upgrade (гейт от параллельного апгрейда)
updates.pipeline_kill_switchboolfalseАварийное отключение автозапуска пайплайна: cms:upgrade отказывается стартовать новые run, пока флаг включён; статус и телеметрия продолжают работать, модуль остаётся включённым
updates.satis_tokenСекрет — хранится только в .env/config, НЕ в settings-store

max_concurrent_runs и smoke_test_timeout_seconds — явные лимиты с дефолтами (критерий «Лимиты и квоты» матрицы v2.2): первый ограничивает конкурентность апгрейдов через лок на уровне БД, второй определяет, во что превращается зависший run (см. «Крайние случаи»). pipeline_kill_switch закрывает критерий «Kill-switch» матрицы — аварийная остановка автоматики без выключения модуля целиком.

API

МетодПутьДоступНазначение
GET/api/v1/admin/updates/availableupdates.manageУстановлено vs доступно, changelog по пакетам
POST/api/v1/admin/updates/runupdates.manageЗапуск обновления (ставит job, не выполняет синхронно)
GET/api/v1/admin/updates/runs/{id}updates.manageСтатус конкретного запуска пайплайна
GET/api/v1/admin/updates/historyupdates.manageЖурнал применённых обновлений (cms_module_history, keyset-пагинация)
POST/api/v1/updates/telemetryподписанный запросПриём подтверждения от флит-дашборда (health-гейт волны)

Компоненты

Filament: страница «Обновления» (список пакетов, changelog, кнопка запуска), индикатор security-обновлений, история запусков с причиной отката при провале. Команды: cms:upgrade --json (полный пайплайн), cms:postupgrade --json (переиндексация/прогрев кеша после мажора), cms:updates:check --json (проверка доступных версий без применения), cms:updates:doctor --json.

Демо-контент: не применимо — модуль не имеет блоков/виджетов, показывать в галерее /_gallery/playground нечего.

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

СобытиеКогдаPayload
UpdateStartedЗапущен пайплайн cms:upgraderun_id, modules[], channel
UpdateSucceededHealth-гейт пройден, обновление зафиксированоrun_id, modules[]
UpdateRolledBackПровал health-гейта, выполнен откат трёх слоёвrun_id, reason
TelemetrySentОтправлена суточная телеметрия на флит-дашбордversions, channel, health_status

Реализует канонический контракт update-client; requires на cms/backup (бэкап перед миграциями) и cms/health (health-гейт после применения). UpdateSucceeded/ UpdateRolledBack резолвятся в уведомление через notification-bus (cms/notifications-bus), если модуль включён. TelemetrySent — исходящий POST на cms/fleet-dashboard, потребляется там как внешний вход, не как внутреннее событие одного процесса. Слушает: собственное расписание проверки версий, health-гейт волны от флит-дашборда (входящий сигнал через API).

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

Сущность/модульКаналНаправлениеЧто происходит
cms/backuprequires → сервис-вызов (канал 4)outБэкап БД перед миграциями, без него пайплайн не стартует
cms/healthrequires → сервис-вызов (канал 4)outHealth-гейт после применения (cms:health:gate), красный статус → откат
cms/notifications-bussuggests → сервис-вызов (канал 3, notification-channel)outУведомление об успехе/провале; выключен → log-fallback
cms/fleet-dashboardвнешний канал (подписанный POST)outСуточная телеметрия версий/канала/health
cms/fleet-dashboard (волна)внешний API (подписанный запрос)inГейт канареечной волны — go/halt для текущего сайта
Satis студиивнешняя интеграция (HTTPS, вне пяти каналов)inМанифест доступных версий и changelog
Реестр модулей ядрачтение через cms/core-contractsinСписок установленных модулей и версий для сверки с манифестом
Событие ModuleEnabled/ModuleDisabled (ядро)событие (канал 1)inЯдро само обновляет cms_module_versions; модуль инвалидирует кеш списка «установлено vs доступно» на странице «Обновления»

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

Очередь updates: весь пайплайн cms:upgrade выполняется джобой (не HTTP-запросом — таймауты php-fpm не позволяют синхронно), идемпотентность обеспечивается журналом cms_update_runs (повторный запуск с тем же run_id не выполняется дважды). Ежесуточная телеметрия и проверка доступных версий — по расписанию через ScheduleRegistrar ядра (auto_check_interval_hours).

Метрики, алерты и ранбук (§15 стандарта)

Метрики: длительность run (весь путь бэкап → dry-run → миграции → health-гейт), доля run со статусом rollback от общего числа запусков за период, задержка ответа Satis на проверку версий. Алерты: run не завершился дольше smoke_test_timeout_seconds на любом шаге — инцидент «обновление зависло»; run завершился rollback — требует разбора причины отката до следующей попытки.

Мини-ранбук:

СимптомЧто проверитьЧем чинится
Обновление зависло (run дольше smoke_test_timeout_seconds)Статус run в cms_update_runs, лог джобы — на каком шаге зависcms:upgrade --rollback --run={id}, затем --resume после устранения причины
Несовместимость версий модулей после апгрейдаcomposer.lock vs манифесты requires установленных модулейcms:updates:doctor --json; откат до совместимого набора версий
Health-гейт красный после апгрейдаcms:health:gate --json — какой модуль не проходит self-testОткат трёх слоёв выполняется автоматически; если не сработал сам — cms:upgrade --rollback вручную
Satis недоступенСетевой доступ до Satis, satis_token в .envЖдать восстановления инфраструктуры студии; UI работает на закешированном списке версий
Двойной запуск cms:upgradeАктивный run в cms_update_runs (лок на уровне БД)Дождаться завершения текущего run либо --rollback, если он завис

Бэкап/рестор: как и все cms_*, таблицы cms_module_versions/cms_module_history (ядро) и cms_update_runs (этот модуль) входят в общий бэкап БД — по ранбуку ядра. После рестора эти таблицы не пересоздаются командой — это чистая история, но обязательна сверка с фактическим composer.lock инсталляции (cms:updates:doctor --json): рестор БД мог откатить состояние к моменту до апгрейда, уже применённого на файловом уровне, или наоборот — рассинхрон нужно обнаружить сразу после рестора, не постфактум.

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

Объёмы: managed-парк — на сайт десятки-сотни установленных модулей, cms_module_history растёт на 1 запись на модуль на обновление (десятки записей в год на активной установке), cms_update_runs — единицы запусков в месяц. Нагрузка модуля не публичная (весь функционал admin-only), бюджет запросов некритичен для page-cache, но страница «Обновления» обязана строиться без N+1 по списку модулей (with() версий разом, не в цикле). Проверка версий с Satis кешируется на короткий TTL (порядка часа), чтобы каждый заход в UI не бил во внешний сервис. Индексы: (module, checked_at) в cms_module_versions, (module, started_at) в cms_module_history, (status, started_at) в cms_update_runs — под фильтр «последний запуск по статусу». Собственных тегов кеша нет — данные о версиях обновляются редко (по расписанию), чтение из БД точечное. На page-cache сайта модуль не влияет; после мажорного обновления cms:postupgrade может прогревать кеш ядра, но это отдельная операция, не тег модуля updates. На время миграций updates.maintenance_mode_enabled подменяет публичные ответы страницей обслуживания — иначе гонка «страница уже недоступна, но page-cache ещё отдаёт старую разметку» после схемных изменений.

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

Векторы, специфичные для модуля:

  • подмена/MITM манифеста Satis — целостность пакетов проверяется composer-механизмом (checksums), satis_token пиннит источник; поддельный changelog не должен приводить к скачиванию непроверенного кода;
  • повторная отправка/подделка /api/v1/updates/telemetry-ответа волны от чужого источника — подпись + TTL (как у cms/fleet-dashboard), запрос без валидной подписи отклоняется до разбора тела;
  • повторный/дублирующий POST /api/v1/admin/updates/run (двойной клик, retry клиента) — Idempotency-Key на мутации с внешним эффектом (§7 стандарта), второй запрос с тем же ключом не создаёт второй run;
  • privilege escalation — запуск обновления доступен только studio-роли, обычный администратор клиента видит статус, но не может стартовать пайплайн (permission, не проверка «по строке роли»);
  • секрет satis_token — исключительно .envconfig(), не в settings-store, не в логах при ошибке скачивания.

Пайплайн не стартует без предварительного успешного бэкапа — гейт на уровне кода, не рекомендация. max_concurrent_runs исключает параллельный cms:upgrade (см. крайние случаи).

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

Рольupdates.view (статус, история)updates.manage (запуск/откат обновления)
Админ клиента✅ только просмотр
Менеджер
Редактор
Studio

Права: updates.view, updates.manage. Запуск обновления — исключительно studio-роль; менеджер и редактор к разделу «Обновления» доступа не имеют вовсе (не операционная зона для контента/заказов).

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

Админ:

  • пустое состояние «нет доступных обновлений» — с датой последней проверки и кнопкой «проверить сейчас», а не пустая таблица без объяснения;
  • массовое действие — обновление нескольких модулей за один запуск (батч в одной джобе с одним бэкапом и одним health-гейтом), не по одному клику на модуль;
  • прогресс пайплайна виден в реальном времени (статус run: бэкап → dry-run → миграции → health-гейт → готово/откат), не «крутилка» без деталей;
  • ошибки — на человеческом языке: «не удалось скачать пакет с Satis — проверьте токен», а не текст исключения;
  • подтверждение перед запуском — модальное окно с описанием необратимых по сути шагов (даунтайм на время миграций, что будет сделано при провале);
  • индикатор «доступно обновление безопасности» — визуально выделен (не теряется среди обычных обновлений).

Посетитель: на время активных миграций — страница обслуживания вместо случайных 500 при частично применённой схеме (maintenance_mode_enabled).

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

  • обрыв процесса посреди миграции (OOM/kill/сеть) → cms_update_runs остаётся в статусе running дольше smoke_test_timeout_seconds; следующий запуск cms:upgrade обнаруживает незавершённый run и отказывается стартовать новый — требует явного --resume или --rollback, не тихого продолжения;
  • несовместимость версий модулей после апгрейда (A требует B ^2.0, B остался на 1.x) → должно ловиться composer install --dry-run до применения; если пропущено — health-чек модуля-потребителя красный → гейт блокирует финализацию, выполняется откат;
  • несовместимость модуля с целевой версией ядра (minimum_core_version модуля выше версии, на которую апгрейдится ядро — конфликт не модуль↔модуль, а модуль↔ядро) → ловится тем же composer install --dry-run (ядро — тоже версионированный пакет через cms/core-contracts): апгрейд ядра при таком конфликте отклоняется целиком до применения миграций со списком несовместимых модулей и требуемых версий, а не накатывается частично с последующим откатом одного модуля;
  • откат при красном health-гейте → откатываются все три слоя (симлинк, composer.lock, схема БД) атомарно одной операцией, не частично — частичный откат оставляет сайт в несогласованном состоянии хуже исходного;
  • недоступность Satis (сеть/студия офлайн) → проверка версий деградирует: UI показывает последний закешированный список с пометкой «не удалось обновить, данные от …», не блокирует остальной функционал сайта;
  • параллельный запуск двух cms:upgrade → лок на уровне БД (уникальный «активный run» в cms_update_runs), которым реализуется настройка max_concurrent_runs; второй запуск отклоняется с понятным сообщением, не стартует параллельный пайплайн поверх первого;
  • точки невозврата пайплайна: шаги cms:upgrade — бэкап БД → composer install --dry-run → применение миграций → health-гейт. До начала применения миграций откат дёшев — симлинк релиза и composer.lock возвращаются на предыдущую версию без участия БД; с момента первой применённой миграции откат становится точкой невозврата на уровне схемы — единственный надёжный путь это полный рестор бэкапа БД, снятого на первом шаге (частичные down()-миграции сторонних модулей как основной путь отката не используются — не гарантированы для всех модулей парка);
  • двойной сабмит кнопки «Запустить обновление» → идемпотентность по run_id/ Idempotency-Key: повторный запрос не создаёт вторую джобу, кнопка блокируется до ответа сервера;
  • выключение модуля посреди операции → в managed-парке disable заблокирован (§3 стандарта помечает модуль обязательным); на standalone-инсталляции принудительное отключение через composer вручную во время выполнения джобы не должно прерывать уже запущенный процесс — доработка джобы идёт до конца, смена статуса модуля не влияет на уже стартовавший run;
  • отсутствие suggests-модуля cms/notifications-bus → уведомление об успехе/провале не отправляется, статус виден только в Filament/логе — пайплайн не блокируется;
  • пустой/огромный changelog → пустой рендерится как «нет описания изменений», многострочный — truncate с «показать полностью», не разрывает вёрстку страницы;
  • обязательность модуля в managed-парке и disable — зафиксировано ревизией 14.07.2026: disable инфра-модулей managed-парка (cms/updates в их числе) недоступен клиентским ролям — доступен только studio-роли, с обязательным подтверждением и записью в аудит; кнопка выключения в админке клиента показывает «модуль обязателен по условиям поддержки». Правило «выключение = деградация» при этом не отменяется: если модуль всё же выключен студией или упал, сайт остаётся жив. См. ревизия ядра 14.07.2026 и §3 стандарта (подраздел «Обязательные модули managed-парка»);
  • противоречивая комбинация настроек: updates.telemetry_enabled=false в managed-парке, где телеметрия обязательна для контроля бэкапов/волн студией → значение клиента форсируется в true на уровне кода (settings-store не позволяет выключить в managed-режиме), иначе студия молча теряет видимость сайта без своего ведома;
  • измерения locale/city/site — таблицы модуля не несут этих измерений (обновление применяется на уровне инсталляции, не сайта); явно зафиксировать: на shared cms/multisite-инсталляции с общим composer.lock нельзя откатить один сайт из нескольких изолированно — ограничение, о котором должен знать оператор перед стартом обновления на мультисайте.

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

Донор: — (новая разработка; процедура и модель версий описаны в /cms-v2/update-center)

Миграция legacy-данных (§16 стандарта): не применимо — донора с боевыми данными нет, cms:updates:import-legacy модулю не нужен.

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

  • [ ] Контрактные тесты: провал health-гейта после обновления откатывает все три слоя (симлинк, composer.lock, схема БД) синхронно;
  • [ ] health-чек модуля = сам health-гейт пайплайна, переиспользуется как smoke-test;
  • [ ] обязательность модуля в managed-парке проверяется при попытке выключения (блокировка с понятным сообщением);
  • [ ] бэкап БД создаётся и верифицируется до начала миграций — без него cms:upgrade не стартует;
  • [ ] телеметрия не содержит данных клиента — только версии/канал/статусы;
  • [ ] права на запуск обновления отделены от простого просмотра доступных версий;
  • [ ] журнал cms_module_history достаточен для восстановления точного состояния парка без внешних источников;
  • [ ] параллельный запуск второго cms:upgrade отклоняется, пока первый активен (лок на уровне БД);
  • [ ] незавершённый run после обрыва процесса блокирует новый запуск до --resume/--rollback;
  • [ ] несовместимость версий модулей ловится composer install --dry-run до применения миграций;
  • [ ] недоступность Satis деградирует до последнего закешированного списка версий, не роняет UI;
  • [ ] updates.telemetry_enabled не может быть выключен клиентом в managed-режиме (контрактный тест на форсинг);
  • [ ] двойной сабмит запуска обновления не создаёт вторую джобу (Idempotency-Key);
  • [ ] апгрейд ядра с модулем, чей minimum_core_version выше целевой версии, блокируется composer install --dry-run до применения миграций, а не откатывается постфактум;
  • [ ] updates.pipeline_kill_switch=true блокирует старт нового run в cms:upgrade; статус страницы «Обновления» и суточная телеметрия при этом продолжают работать;
  • [ ] матрица ролей: админ клиента видит статус через updates.view, но POST /api/v1/admin/updates/run под его правами отклоняется 403 — запуск доступен только studio-роли (updates.manage); менеджер/редактор не получают доступа вовсе;
  • [ ] после рестора бэкапа БД cms_module_versions/cms_module_history/cms_update_runs восстановлены, и запущенная сверка (cms:updates:doctor --json) подтверждает консистентность истории с фактическим composer.lock инсталляции;
  • [ ] контрактный набор cms-testing зелёный, пакет протестирован в testbench-изоляции;
  • [ ] feature-тест на каждый роут API; тестовая БД только updates_test, migrate:fresh/refresh/reset запрещены.

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