Тема
ТЗ — Центр обновлений (клиент) (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/run | modules[] (пакеты для обновления), channel | FormRequest whitelist: modules.* — только установленный пакет из cms_module_versions, channel — enum stable/beta; неизвестный пакет → 422 |
| Манифест Satis студии (внешний источник) | доступные версии, changelog, severity | HTTPS + satis_token; схема манифеста валидируется перед разбором, битый JSON — отказ без падения проверки |
POST /api/v1/updates/telemetry-ответ волны от cms/fleet-dashboard | wave_id, статус гейта (go/halt) | подписанный запрос, TTL как у cms/fleet-dashboard |
Событие ModuleEnabled/ModuleDisabled (ядро, канал 1) | module, version | внутреннее событие, не пользовательский ввод — только консистентность payload |
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Filament UI | установлено/доступно, changelog, статус запуска, история | Blade/Livewire через сервис модуля (не запросы из шаблона) |
/api/v1/admin/updates/* | конверт data/meta | JSON 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.channel | string | "stable" | — | Канал обновлений (stable/beta) |
updates.auto_check_interval_hours | int | 24 | — | Периодичность проверки доступных версий |
updates.telemetry_enabled | bool | true | — | Отправка телеметрии на флит-дашборд; в managed-парке форсируется true на уровне кода, клиент изменить не может (см. крайний случай ниже) |
updates.smoke_test_timeout_seconds | int | 60 | — | Таймаут health-гейта после применения обновления |
updates.maintenance_mode_enabled | bool | true | — | Показывать страницу «идёт обновление» на время миграций вместо риска 500 |
updates.notify_on | array | ["success","failure"] | — | На какие исходы пайплайна отправлять уведомление через notifications-bus |
updates.max_concurrent_runs | int | 1 | — | Лимит одновременных запусков cms:upgrade (гейт от параллельного апгрейда) |
updates.pipeline_kill_switch | bool | false | — | Аварийное отключение автозапуска пайплайна: 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/available | updates.manage | Установлено vs доступно, changelog по пакетам |
| POST | /api/v1/admin/updates/run | updates.manage | Запуск обновления (ставит job, не выполняет синхронно) |
| GET | /api/v1/admin/updates/runs/{id} | updates.manage | Статус конкретного запуска пайплайна |
| GET | /api/v1/admin/updates/history | updates.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:upgrade | run_id, modules[], channel |
UpdateSucceeded | Health-гейт пройден, обновление зафиксировано | 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/backup | requires → сервис-вызов (канал 4) | out | Бэкап БД перед миграциями, без него пайплайн не стартует |
cms/health | requires → сервис-вызов (канал 4) | out | Health-гейт после применения (cms:health:gate), красный статус → откат |
cms/notifications-bus | suggests → сервис-вызов (канал 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-contracts | in | Список установленных модулей и версий для сверки с манифестом |
Событие 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— исключительно.env→config(), не в 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запрещены.