Тема
ТЗ — Маркетплейс модулей (cms/marketplace-modules)
Слой: 🔵 инфра-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке
Назначение и возможности
Каталог модулей и тем студии внутри админки: просмотр доступных по лицензии сайта пакетов и запуск установки через cms/updates, а не напрямую composer-командой из UI. Сейчас маркетплейс закрытый — витрина показывает только модули студии, сторонние разработчики не публикуют пакеты.
- Каталог модулей/тем с описанием, зрелостью, зависимостями (
requires/suggests); - отметка «доступно по лицензии сайта» / «требует лицензию» на каждый пакет;
- установка модуля — это заявка в
cms/updates(сам маркетплейс composer не вызывает); - лицензии выдаются per-сайт (домен привязан к токену Satis, см. update-center);
- поиск и фильтрация каталога (по слою инфра/функц, по зрелости, по зависимостям);
- статус «уже установлен» / «установлен, но выключен» для каждого пакета парка;
- предупреждение об истекающей лицензии до того, как пакет станет недоступен;
- каталог — зеркало Satis: модуль не является источником истины по лицензиям и ценам.
Зависимости и выключение
requires: cms/updates · provides: marketplace-catalog
При выключении каталог модулей недоступен из UI — установка новых модулей возможна только вручную через cms/updates/composer напрямую, без витрины.
Стоимость внешних API. Satis студии — внутренняя инфраструктура (собственный Composer-репозиторий), не тарифицируемый внешний сервис: обращения к нему не считаются квотой и не имеют денежной стоимости за запрос. Отдельно от этого — сами лицензии на платные пакеты имеют коммерческую модель студии (продажа/продление доступа к пакету), но это бизнес-условие, не API-квота: истёкшая лицензия — не «исчерпание лимита запросов», а прекращение доступа к конкретному пакету (см. «Крайние случаи»).
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_marketplace_catalog | package, name, layer, maturity, description, requires (json) | Кеш каталога, синхронизируется с Satis студии |
cms_marketplace_licenses | site_id, package, licensed_at, expires_at | Лицензии, доступные текущему сайту |
requires — JSON-столбец с cast array; layer/maturity — enum → PHP Enum. site_id в cms_marketplace_licenses — constrained('cms_sites')->index() (если cms/multisite включён, иначе — единственный сайт установки); уникальный индекс (site_id, package). cms_marketplace_catalog.package — уникальный индекс (по нему формируется заявка на установку и проверяется существование пакета).
ПДн-паспорт: обе таблицы не содержат персональных данных — site_id, package, licensed_at/expires_at и поля каталога (name, layer, maturity, description, requires) — идентификаторы инсталляции и коммерческие атрибуты лицензии/пакета, не данные субъекта. Участия в «выгрузить всё по субъекту» / «забыть по запросу» (152-ФЗ) модуль не несёт, UserDeleted не слушает.
Входные и выходные данные
Входы
| Источник | Данные/поля | Чем валидируется |
|---|---|---|
| Filament: фильтр/поиск каталога | layer, maturity, q (поисковая строка) | FormRequest whitelist параметров, неизвестный фильтр → 422 |
Filament: кнопка «Установить» / POST .../{package}/install | package (slug пакета) | permission marketplace-modules.manage; package — обязан существовать в cms_marketplace_catalog и быть лицензирован для текущего сайта |
| Манифест Satis студии | список пакетов, зрелость, зависимости, слой | HTTPS + токен (тот же канал доступа, что у cms/updates), схема каталога валидируется перед разбором |
| Реестр лицензий студии (внешний источник, синк с Satis) | site_id, package, expires_at | привязка домена к токену, как в update-center; протухшая лицензия — пакет помечается недоступным |
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Filament UI | карточки каталога, статус «доступно»/«требует лицензию»/«установлен» | Blade через сервис модуля |
/api/v1/admin/marketplace-modules/* | конверт data/meta | JSON REST, keyset-пагинация каталога |
cms/updates | заявка на установку пакета | сервис-вызов (requires, канал 4) |
cms_marketplace_catalog/cms_marketplace_licenses | синхронизированный каталог и лицензии | БД |
Телеметрия к Satis (прозрачность для клиента). При запросе каталога и при проверке лицензии на Satis студии уходит: домен/идентификатор сайта, токен доступа сайта (тот же канал, что у cms/updates), запрашиваемый package. Содержимое сайта клиента — контент, посетители, их данные — не передаётся: обмен ограничен метаданными установки и лицензирования. Это не персональные данные посетителей сайта (субъект обмена — инсталляция, не человек); состав передаваемых данных задокументирован в docs/module.md как факт интеграции, не скрытый сбор.
Настройки (группа marketplace-modules)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
marketplace-modules.catalog_sync_interval_hours | int | 24 | — | Периодичность синхронизации каталога с Satis |
marketplace-modules.show_unlicensed | bool | true | — | Показывать в каталоге пакеты без лицензии (как «недоступно») |
marketplace-modules.license_expiry_warning_days | int | 14 | — | За сколько дней до истечения лицензии показывать предупреждение в карточке |
marketplace-modules.catalog_page_size | int | 24 | — | Размер страницы каталога (keyset-пагинация) |
marketplace-modules.sync_kill_switch | bool | false | — | Аварийное отключение автосинхронизации каталога с Satis: джоба по расписанию не запускается, ручная кнопка «Синхронизировать сейчас» тоже блокируется |
Лимиты и квоты. catalog_page_size — единственный количественный лимит модуля (размер страницы каталога), дефолт задан; при достижении — обычная keyset-пагинация (следующая страница по курсору), не деградация и не ошибка. sync_kill_switch — не лимит, а аварийный выключатель на случай проблем на стороне Satis (см. «Фоновая работа» и «Крайние случаи»), чтобы не долбить его повторными запросами.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/admin/marketplace-modules/catalog | marketplace-modules.manage | Каталог модулей/тем с фильтрами (keyset-пагинация) |
| GET | /api/v1/admin/marketplace-modules/catalog/{package} | marketplace-modules.manage | Карточка пакета (описание, зависимости, лицензия) |
| POST | /api/v1/admin/marketplace-modules/catalog/{package}/install | marketplace-modules.manage | Заявка на установку (делегирует в cms/updates) |
Компоненты
Filament: страница каталога (карточки модулей, фильтр по слою/зрелости, бейдж лицензии), кнопка «Установить» (создаёт заявку в центре обновлений, не выполняет установку сама). Команды: cms:marketplace-modules:sync --json (синхронизация каталога с Satis), cms:marketplace-modules:doctor --json.
Демо-сидеры: не применимо — модуль не имеет собственных блоков/виджетов, каталог и лицензии не участвуют в demo-галерее блоков и playground.
События и обмен
| Событие | Когда | Payload |
|---|---|---|
MarketplaceCatalogSynced | Каталог обновлён с Satis | packages_count |
MarketplaceInstallRequested | Заявка на установку пакета отправлена в cms/updates | package, user_id |
requires: cms/updates — установка пакета всегда идёт через публичный сервис центра обновлений (MarketplaceInstallRequested → заявка в cms/updates), сам маркетплейс composer не вызывает. Реализует контракт marketplace-catalog. FilterBus не используется. Слушает только собственное расписание синхронизации каталога с Satis.
Таблица взаимодействий
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/updates | requires → сервис-вызов (канал 4) | out | Заявка на установку пакета делегируется в пайплайн cms:upgrade |
| Satis студии | внешняя интеграция (HTTPS, вне пяти каналов) | in | Синхронизация каталога и лицензий по расписанию |
| Реестр модулей ядра | чтение через cms/core-contracts | in | Статус «уже установлен» / «установлен, но выключен» для каждого пакета |
Фоновая работа
Очередь marketplace-modules: синхронизация каталога с Satis — идемпотентная джоба по расписанию (catalog_sync_interval_hours) через ScheduleRegistrar ядра. Заявка на установку — тоже job, делегируется в очередь cms/updates.
Kill-switch sync_kill_switch=true останавливает джобу синхронизации целиком (не запускается по расписанию) и блокирует ручную кнопку «Синхронизировать сейчас» в Filament — каталог остаётся как есть до выключения свитча обратно. Сценарий: проблемы на стороне Satis — не долбить его повторными запросами, пока не починят.
Метрики и алерты: возраст последней успешной синхронизации каталога (относительно catalog_sync_interval_hours), доля заявок на установку, отклонённых из-за истёкшей лицензии, длительность джобы синхронизации. Алерт — синхронизация не проходила дольше catalog_sync_interval_hours × 2 (пока включён sync_kill_switch алерт подавляется — это осознанная пауза, не сбой).
Ранбук (типовые инциденты):
| Симптом | Что проверить | Команда-лечение |
|---|---|---|
| Каталог пуст / не синхронизирован | статус очереди marketplace-modules, лог последней джобы, не включён ли sync_kill_switch | cms:marketplace-modules:sync --json (вручную, если свитч выключен) |
| Лицензия истекла посреди установки | expires_at в cms_marketplace_licenses на момент заявки vs на момент старта cms:upgrade | не чинится автоматически — заявка отклоняется, клиенту нужна новая лицензия от студии |
| Рассинхрон с Satis (локальный каталог устарел) | возраст MarketplaceCatalogSynced, статус последнего запроса к Satis в логе | cms:marketplace-modules:sync --json; не помогает → cms:marketplace-modules:doctor --json |
| Недоступность Satis при синхронизации | сетевой доступ до Satis студии, не протух ли токен | джоба сама деградирует до «оставить прежний каталог» — ручных действий не требуется; при длительной недоступности включить sync_kill_switch, чтобы не спамить попытками |
Бэкап/рестор: cms_marketplace_catalog и cms_marketplace_licenses — обе таблицы кеш/зеркало Satis, не источник истины: при потере (не попали в бэкап или рестор сломан) восстанавливаются без даунтайма — каталог прогоном cms:marketplace-modules:sync --json, лицензии — повторной синхронизацией из реестра студии (тем же каналом, что у update-center). Специального шага в процедуре рестора сайта не требуется.
Производительность и кеш
Объёмы: закрытый каталог студии — десятки-сотни пакетов (модули + темы), не тысячи; лицензий — по одной-две записи на сайт на используемый платный пакет. Горячий путь — рендер каталога в Filament (карточки с фильтрами) — должен строиться без N+1 по requires/зависимостям каждого пакета (JSON-столбец читается разом, не отдельным запросом на карточку). Тег marketplace-modules.catalog — кешированный каталог (зеркало Satis) инвалидируется событием MarketplaceCatalogSynced; на page-cache сайта не влияет, только на админ-каталог. Проверка лицензии при заявке на установку — прямой запрос к cms_marketplace_licenses по индексу (site_id, package), не через кеш (цена устаревшей лицензии выше цены лишнего запроса).
Безопасность
Filament/API — marketplace-modules.manage на просмотр каталога и заявку на установку. Лицензия проверяется на сервере при формировании заявки, не только визуально в UI — недоступный по лицензии пакет не устанавливается даже прямым вызовом API. Закрытость маркетплейса обеспечивается источником каталога (Satis), не обходится клиентской настройкой show_unlicensed. Специфичные векторы: подмена package в теле запроса на пакет вне каталога/чужого слоя — whitelist по существующей записи cms_marketplace_catalog, не по свободной строке; попытка установить пакет с истёкшей expires_at — сервер отклоняет заявку с понятным сообщением, а не полагается на то, что UI её не покажет.
Права: marketplace-modules.view, marketplace-modules.manage.
Матрица ролей:
| Роль | .view (каталог) | .manage (установка) |
|---|---|---|
| Studio (админ студии) | есть | есть |
| Администратор сайта | есть | есть |
| Менеджер | есть (только просмотр) | нет |
| Редактор контента клиента | нет | нет |
Установка модулей — решение уровня владельца инсталляции, не редакторской роли: .manage выдаётся администратору сайта и studio; менеджер видит каталог для оценки, но кнопка «Установить» ему недоступна ни в UI, ни при прямом вызове API — сервер отклоняет запрос без permission, не полагается на скрытие кнопки. Редактору контента клиента каталог не показывается вовсе (вне его зоны ответственности).
UX-требования
Админ: пустое состояние «каталог пуст» с подсказкой «последняя синхронизация — …» и кнопкой ручной синхронизации; карточка пакета с истекающей лицензией — заметное предупреждение за license_expiry_warning_days до истечения, не сюрприз в день X; ошибка установки — человеческое сообщение («лицензия на этот пакет истекла — свяжитесь со студией», не «403 Forbidden»); статус «уже установлен, но выключен» отличим от «не установлен» одним взглядом на карточку; массового действия по установке нет — установка всегда одного пакета за раз через cms/updates, что стоит явно показывать пользователю (не создавать иллюзию батч-установки).
Крайние случаи и типовые баги
- двойной клик «Установить» → идемпотентность заявки: повторный клик до ответа сервера не создаёт вторую заявку в
cms/updates— кнопка блокируется на клиенте и сервер игнорирует дублирующийMarketplaceInstallRequestedв короткое окно; - лицензия истекает посреди установки (заявка отправлена, пока лицензия ещё действовала, но
cms:upgradeстартует позже) → проверка лицензии выполняется и на формировании заявки, и повторно перед стартом установки вcms/updates— окно между заявкой и фактическим применением не должно давать установку без действующей лицензии; - рассинхрон каталога (Satis обновил список, локальный кеш устарел) → каталог показывает пометку возраста данных («обновлено N часов назад»), health-чек модуля фиксирует протухшую синхронизацию отдельно от общего статуса;
- недоступность Satis при синхронизации → джоба синхронизации деградирует до «оставить прежний каталог», не затирает его пустым ответом; ошибка логируется, не падает молча;
- отсутствие
cms/updates(requires не удовлетворён — некорректная инсталляция) → манифест-гейт §3 стандарта не даёт модулю включиться без своей зависимости — сам сценарий не должен возникать в проде, но self-test enable обязан явно на это указывать; - выключение модуля посреди установки → уже отправленная заявка в
cms/updatesпродолжает обрабатываться независимо (владение процессом установки — уcms/updates, не у маркетплейса) — выключение витрины не прерывает уже идущийcms:upgrade; - пустой каталог (первая установка, синхронизация ещё не прошла) → «каталог синхронизируется, зайдите позже» вместо пустого экрана без объяснения;
- огромный каталог (гипотетический рост числа пакетов студии) → keyset-пагинация и индексы по
layer/maturityуже заложены — фильтрация не деградирует до full scan; - измерения locale/city/site — каталог и лицензии не мультиязычны; лицензия привязана к
site_id(приcms/multisite) — на инсталляции с несколькими сайтами каждый сайт видит только свои лицензии, общий каталог пакетов один на всех; - противоречивая комбинация настроек:
show_unlicensed=false, но лицензия истекает во время сессии администратора (пакет был виден, потом пропал из каталога без объяснения) → карточка не должна резко исчезать — переход в статус «лицензия истекла» нагляднее, чем исчезновение записи.
Донорский код
Донор: — (новая разработка)
Миграция legacy-данных (§16 стандарта) — не применимо: у модуля нет донора, каталог и лицензии — новая сущность студии, данных на старых площадках для переноса не существует. Команда cms:marketplace-modules:import-legacy не создаётся.
Тесты и приёмка
- [ ] Контрактные тесты: установка пакета из каталога создаёт заявку в
cms/updates, а не вызывает composer напрямую; - [ ] health-чек модуля проверяет актуальность синхронизации каталога (не протух дольше интервала);
- [ ] деградация при выключении — установка модулей остаётся возможной вручную через
cms/updates; - [ ] лицензия проверяется на сервере при заявке на установку, не только визуально в UI;
- [ ] лицензия перепроверяется повторно перед фактическим применением в
cms/updates, не только на формировании заявки; - [ ] двойной клик «Установить» не создаёт две заявки на один пакет;
- [ ] права на установку отделены от простого просмотра каталога;
- [ ] закрытость маркетплейса (только пакеты студии) не может быть обойдена изменением настройки клиентом;
- [ ] недоступность Satis при плановой синхронизации не затирает существующий каталог;
- [ ] контрактный набор
cms-testingзелёный, пакет протестирован в testbench-изоляции; - [ ] feature-тест на каждый роут API; тестовая БД только
marketplace-modules_test,migrate:fresh/refresh/resetзапрещены; - [ ]
sync_kill_switch=trueостанавливает джобу синхронизации по расписанию и блокирует ручную кнопку «Синхронизировать сейчас» — и в UI, и при прямом вызове команды/API; - [ ] роли разведены: менеджер видит каталог, но не может инициировать установку ни через UI, ни через API; редактор контента клиента каталог не видит вовсе;
- [ ] состав телеметрии к Satis (домен/токен/package) задокументирован в
docs/module.mdи не содержит содержимого сайта клиента или персональных данных посетителей.