Администрирование платформы
Простыми словами: это диспетчерская всей платформы, а не отдельной организации — шесть страниц ниже (Organizations, Users, Products, Platform Roles, Staff, Audit Log) позволяют собственной команде P4P управлять каждой компанией, использующей платформу, а не только своей. Это отдельная область от страниц «Workspace», которые видит обычный участник организации.
Всё под /platform/*. Доступно с точки входа Platform Administration на /dashboard, но, как и любая страница этого модуля, требует platform:admin:access (middleware/platform-admin-access.ts), не только SUPER_ADMIN. До 2026-08-14 единственным гейтом здесь было наличие любой PlatformRole вообще — это значило, что сотрудник платформы, чья реальная работа — Персонал (platform:staffing:*, /team/*), всё равно мог зайти в эту диспетчерскую. Каждый отдельный роут уже был закрыт своим правом, так что реально ничего не было уязвимо — но у такого человека и не было причины вообще открывать этот раздел. platform:admin:access — это базовый гейт входа теперь, обязателен ДОПОЛНИТЕЛЬНО к праву каждого раздела (единообразное разделение read/manage по всему модулю — просмотр без права :manage разрешён; изменяющее действие скрыто или заблокировано). Полное описание, включая пару роутов admin-users.controller.ts, намеренно оставленных без этого гейта, потому что /team/members (Персонал) вызывает их напрямую («Send on leave»/ «Return from leave») — см. запись от 2026-08-14 в docs/iam/PERMISSIONS_CATALOG.md.
Все контроллеры здесь стоят за @UseGuards(PlatformRoleGuard) на уровне контроллера (самодостаточно, без зависимости от порядка с TenantGuard — в отличие от PermissionsGuard уровня организации, см. IAM). SUPER_ADMIN безусловно обходит PlatformRoleGuard.
Как сделать...
Сгруппировано так же, как технические разделы ниже — иконка помощи каждой страницы ведёт прямо в свою группу, а не во весь этот список.
Дашборд
Попасть в раздел — /platform/dashboard, страница, которая открывается по кнопке Администрирование платформы: по карточке на раздел (Рабочие пространства, Пользователи, P4P персонал, Продукты, Роли), у каждой свои живые счётчики. Показываются только те разделы, которые ты реально можешь читать — администратор с узкой платформенной ролью видит короткий хаб, а не блёклые карточки.
Посмотреть, что только что делали другие администраторы — панель Последняя активность под карточками: пять свежих записей аудита, у каждой автор и сколько времени прошло, плюс красная пометка Ошибка у того, что не удалось. Наведи на строку — увидишь её целиком и причину ошибки. Показать всё открывает полный Журнал аудита. Панели нужно право platform:audit:read, без него её нет совсем.
Прочитать счётчик «—» — это число не загрузилось либо твоя платформенная роль не может читать этот раздел; … значит, что оно ещё грузится. Каждая плитка падает отдельно, не роняя хаб.
Организации
Создать организацию со стороны платформы — «Organizations» → Новое рабочее пространство → Название/URL пространства/Тип → Создать. См. гайд по полям.
Забрать управление зависшей организацией — открой организацию → Стать владельцем → укажи Причину → Продолжить → впиши название пространства для подтверждения. Только SUPER_ADMIN. См. гайд по полям.
Переопределить язык организации по умолчанию — открой организацию → Язык по умолчанию → выбери из списка. Только SUPER_ADMIN — переопределяет ту же настройку, которой изнутри своего пространства управляет OWNER организации (см. IAM → Настройки организации), не требуя членства.
Дать или забрать у организации доступ к продукту — открой организацию → её раздел Products → Активировать/Деактивировать нужный продукт. (Сама страница «Products» редактирует только каталог — название/описание/активность — а не то, кто на что подписан.)
Создать организацию — гайд по полям
- Название (обязательно) — от 2 до 100 символов.
- URL пространства (обязательно) — от 2 до 50 символов, только строчные латинские буквы, цифры и дефисы. Автоматически заполняется из Названия по мере ввода (например, «Acme Corp» →
acme-corp) — начни редактировать его сама, и это автозаполнение отключается насовсем до конца работы с этим диалогом, даже если потом снова изменишь Название. Должен быть уникален по всей платформе; занятый вариант ловится только при отправке, не по ходу набора текста. - Тип — Company / Individual / Agency / Service provider / Platform / Personal. Только организации типа Personal создаются сразу Active; любой другой тип начинается со статуса Pending (значения статусов см. на собственной странице деталей организации).
Стать владельцем — гайд по полям
Эта кнопка появляется только рядом с тем, кто прямо сейчас держит роль OWNER, и только для SUPER_ADMIN, который сам этим владельцем не является. Она не даёт передать владение выбранному участнику — клик по ней всегда делает новым владельцем именно тебя, администратора, который нажал (при необходимости присоединяя тебя к организации, если ты ещё не её участник); прежний владелец понижается до обычного MEMBER, а не удаляется.
- Причина (шаг 1, необязательно) — свободный текст, чисто запись для того, кто потом читает журнал аудита.
- Текст подтверждения (шаг 2, обязательно) — нужно ввести точное Название организации, прежде чем финальная кнопка Стать владельцем перестанет быть неактивной.
Пользователи
Удалить аккаунт пользователя — «Users» → строка → Удалить → подтвердить.
Убрать кого-то из персонала платформы — открой пользователя → Убрать из команды. Закрывает его членство в штате и все платформенные роли, но не удаляет сам аккаунт.
Дать кому-то платформенную роль или сделать новым владельцем платформы — открой пользователя → Роли платформы → отметь роли → Сохранить роли. Передача самого владения — отдельная кнопка на той же странице, Передать владение этому человеку.
Отправить сотрудника в отпуск или вернуть из него — этого действия здесь нет вообще; это меню «⋯» на его профиле в Персонал → Сотрудники.
Приглашения в персонал платформы
Пригласить кого-то во внутреннюю команду P4P — «Staff» → Пригласить в персонал платформы → Email, при желании заранее отметь Платформенные роли → Отправить приглашение. См. гайд по полям.
Пригласить персонал платформы — гайд по полям
Это массовое приглашение: динамический список строк, у каждой своя пара Email + Платформенные роли, с кнопками +/– для добавления и удаления строк.
- Email (обязательно, по строке) — проверки формата нет, кроме «не пусто»; некорректный адрес, или уже занятый персоналом/приглашением, ловится только при отправке — и валит только эту строку, а не всю пачку.
- Платформенные роли (необязательно, по строке) — оставить все пустыми совершенно нормально: приглашённый просто начнёт без единой платформенной роли, что является нормальным состоянием, а не пробелом — доступ к Team Management приходит из самого факта, что человек в персонале платформы, независимо от конкретной роли. Это поле — чисто способ заранее назначить роли. Посевные роли Owner/
SUPER_ADMINникогда не появляются в этом списке — их нельзя выдать приглашением, только через «Стать владельцем» или прямую передачу роли на странице пользователя. - Отправить приглашение отправляет всю пачку одним запросом. Отправка заблокирована, пока хоть у одной строки есть ошибка валидации или отказ сервера, и разблокируется в момент правки именно этой строки; провалившаяся строка показывает свою причину инлайн, а не один тост на всю пачку.
Продукты и подписки
Отредактировать, как продукт выглядит в каталоге — «Products» → «⋯» строки → Редактировать → Название/Описание → Сохранить изменения. Включение/выключение продукта по всей платформе — отдельное действие из того же меню; выдача/отзыв доступа конкретной организации делается со страницы этой организации — см. Организации выше. См. гайд по полям.
Отредактировать продукт — гайд по полям
- Название — обязательно, от 2 до 100 символов.
- Описание — необязательно, до 2000 символов.
- Больше здесь ничего не редактируется — этот диалог трогает только то, как продукт описан в каталоге.
- Активировать/деактивировать (по всей платформе) не часть этого диалога — это собственное действие-переключатель в строке, обратимое, без подтверждения. Выключение продукта скрывает его сразу для всех организаций, независимо от статуса подписки каждой из них.
Роли
Создать собственную платформенную роль — «Roles» → Создать роль → назови, выбери права → сохранить. См. гайд по полям.
Отредактировать, дублировать или удалить роль — меню ⋯ роли → Редактировать, Дублировать или Удалить. Редактировать и Удалить не предлагаются для встроенной системной роли, поскольку её нельзя ни переименовать, ни удалить — Дублировать и Просмотр прав по-прежнему работают. Назначение/снятие роли у пользователя происходит со страницы самого пользователя — см. Пользователи выше. Дублировать заранее заполняет ту же форму правами существующей роли (в том числе системной), назвав её «Копия ⟨оригинал⟩» — ничего не сохраняется, пока не отправишь форму сама.
Создать/отредактировать платформенную роль — гайд по полям
Отдельная страница, не диалог, чтобы у списка прав было место развернуться. Встроенные системные роли (например, SUPER_ADMIN) показаны в том же списке, но у них вообще нет действия Редактировать, и удалить их нельзя никогда; Дублировать и Просмотр прав по-прежнему работают на них.
- Название — обязательно, до 100 символов, минимум 2. Должно быть уникальным среди всех платформенных ролей.
- Описание — необязательно, до 2000 символов.
- Права — используй поле поиска или открывай по одному разделу за раз (Organizations, Users, Roles, Персонал и так далее). Роль без единого отмеченного права допустима — она просто ничего не даёт. Чекбокс на заголовке каждого раздела выбирает или снимает все его права одним кликом; второй чекбокс над полем поиска делает то же самое сразу по всем разделам, учитывая активный фильтр — так что «выбрать всё» на отфильтрованном виде затрагивает только то, что сейчас показано.
- Связанная роль P4P Internal (необязательно) — выбор, ограниченный собственными ролями P4P Internal (Team Manager/Team Admin/Team Member и так далее); см. выше о том, что это реально делает при назначении.
- Сохранить/Создать активируется, только когда что-то реально отличается от загруженного.
Журнал аудита
Настраивать нечего — это только просмотр. Фильтруй по актору/действию/периоду прямо на странице; см. Журнал аудита ниже о том, что именно логируется и почему это отдельно от обычной админ-работы.
Анонсы
Напишите короткий Заголовок и Текст, затем Отправить. Между вами и реальной отправкой стоит диалог подтверждения — это уходит сразу в колокольчик уведомлений и почту каждого сотрудника P4P, и отменить нельзя. Отдельного экрана истории здесь нет — запись о том, кто и что отправил, живёт в Журнале аудита ниже (platform.announcement.sent), как и любое другое платформенное админ-действие.
AI-ассистенты
Открыть платформенных ассистентов — Настройки → AI-ассистенты. По карточке на глобального ассистента; сегодня он один — Platform Assistant.
Изменить то, что ему сказано — открой его → Поведение → Инструкции → Сохранить изменения. Не перечисляй там его инструменты: платформа и так передаёт модели точный список того, чем ей можно пользоваться, решая это для каждого человека по его правам, — переписанная руками копия устаревает в тот момент, когда добавляют новый инструмент. Ровно это и случилось до 10.09.2026.
Сменить модель — открой его → Модель. Предлагаются только модели, включённые в каталоге платформы: на выключенной падает любой ответ. Это самая ценная настройка здесь — она позволяет перевести ассистента по умолчанию на более дешёвую или более новую модель без деплоя.
Переименовать — открой его → Название и описание. Ни название, ни описание модели не отправляются; они нужны людям, чтобы его узнавать.
Посмотреть, кто его менял — ссылка внизу списка или журнал аудита с фильтром AI-ассистенты (ai_assistant.updated). Каждое изменение здесь ложится сразу на все пространства — поэтому оно и записывается.
Что изменить нельзя и почему
Три блока на странице помечены Управляется платформой и не имеют полей. Они не спрятаны, потому что тот, кто знает страницу обычного агента, придёт их искать:
- Инструменты — встроенные инструменты платформы включены всегда и отвечают правами спрашивающего, выдавать нечего. Внешнее MCP-подключение принадлежит одному пространству, поэтому грант на общем ассистенте ничего не дал бы всем остальным.
- Эталонные изображения — закреплённая картинка достаётся от имени спрашивающего, внутри его пространства; загруженная здесь не откроется ни у одной другой организации.
- Автоматизация и расписания — расписание принадлежит одной организации, а у общего ассистента её нет. Разрешить работу без человека означало бы, что любое пространство сможет гонять его в фоне от своего имени.
Параметры генерации (temperature, top-p, максимум токенов ответа — Решение 20) на этой странице тоже не показаны, в отличие от обычного агента рабочего пространства, у которого они есть в Конфигурации → Модель. У самой записи те же три колонки, что и у любого агента — форма этой страницы их просто пока не предлагает.
AI-модели
Открыть каталог — Настройки → AI-модели. По строке на каждую модель, которую могут выбрать все пространства.
Починить замолчавшую модель — открой её → выбери Идентификатор из списка OpenRouter → Сохранить. Поставщики убирают идентификаторы моделей, и убранный не падает с ошибкой: возвращается пустой ответ, ассистенты просто перестают отвечать, и в логах ничего об этом нет. Список — самого OpenRouter, поэтому опечатку или снятый идентификатор выбрать нельзя. Если сохранённого идентификатора в списке уже нет, он показывается первым с пометкой Нет в списке OpenRouter.
Добавить модель — кнопка + → выбери Провайдера, затем модель. Для OpenRouter название, цена и то, читает ли модель картинки, подставляются из его списка и остаются редактируемыми. Модель, которая не умеет вызывать инструменты, помечается: с ней ассистент не достанет ни доску, ни календарь, ни документацию. Провайдера у существующей модели сменить нельзя — это уже другая модель, добавь новую.
Проверить модель на сайте провайдера — маленькая иконка со стрелкой в её строке или ссылка под полем Идентификатор в диалоге. Открывается в новой вкладке: для модели OpenRouter это её собственная страница (цена, лимиты, не снимают ли её), для Claude, GPT и Gemini — список моделей самого провайдера, потому что постоянной страницы на каждую модель у них нет. У модели с недописанным идентификатором ссылки нет, вместо ссылки на страницу с ошибкой.
Вывести модель из обращения — открой её → выключи Доступность. Она останется в каталоге и исчезнет из выбора у агентов. Убрать удаляет её совсем; у агентов, уже выбравших её, настройка останется до ручной смены, и следующий запуск у них упадёт.
Отметить, что модель принимает картинки — включи Картинки. Чат откажется прикладывать изображение к агенту, чья модель так не отмечена, вместо того чтобы отправить его и получить ошибку от поставщика.
Отметить, какие параметры генерации модель принимает — переключатели Параметры генерации (Temperature, Top P, Максимум токенов ответа), по умолчанию включены. Выключи тот, что модель прямо отклоняет (обычно так у моделей-«рассуждателей» с temperature) — тогда сохранённое значение агента на этом конкретном параметре просто никогда не доходит до вызовов этой модели. Для OpenRouter они подставляются из списка самого провайдера, как и Картинки, и остаются редактируемыми.
Изменение действует со следующего сообщения — уже собранные агенты пересобираются, а не остаются на старой модели.
Дашборд (/platform/dashboard)
Простыми словами: прихожая Администрирования платформы — навигационные карточки с реальными числами на них плюс пять последних административных событий. Сам /platform — это только редирект сюда.
Намеренно не аналитический экран: ни графиков, ни трендов. Числа нужны, чтобы карточка сказала, стоит ли её открывать (78 пользователей, 0 ожидающих приглашений), а не чтобы эту страницу изучали.
- Каждый счётчик настоящий и берётся параллельно из тех же списочных эндпоинтов, которыми пользуются сами разделы (
/admin/organizations,/admin/users,/admin/users/platform-staff,/admin/products,/admin/platform-roles,/admin/users/platform-staff-invitations), каждый — под тем же правом, что и его карточка, и каждый деградирует в—самостоятельно. - Последняя активность — это
GET /admin/audit-log?limit=5(platform:audit:read), с той же таблицей переводов действий, что и страница Журнала аудита: неизвестный код действия показывается как есть, а не превращается в пустую строку. - Карточки фильтруются по правам, а не блокируются. Сравни с главной самого рабочего пространства, где все три карточки показаны, а недоступные просто блёклые: короткий фиксированный набор имеет смысл показывать целиком, длинный — нет.
- Чтобы попасть на страницу вообще, нужно
platform:admin:access(middleware/platform-admin-access.ts).
Организации (/platform/organizations, /platform/organizations/[id])
Простыми словами: все компании/агентства/частные лица, использующие платформу, с поиском и управлением из одного списка. Включает аварийное действие «взять владение» на случай, если реальный владелец организации недоступен.
admin-organizations.controller.ts, смонтирован на admin/organizations.
- Список / детали / участники (
GET) —platform:organizations:read. - Восстановление заархивированной организации (
POST :orgId/restore, переводитARCHIVED→ACTIVE, логируется какorg.restored) — подтверждено (2026-08-04, поиск поapps/portal): точки входа в интерфейсе нет. Единственный UI «restore», который вообще существует в портале — это несвязанная фича Shared Documents (восстановление мягко удалённого файла из корзины,POST /organizations/{id}/files/{fileId}/restore) — не путать эти два. Раз у архивации тоже нет UI, организация, ставшаяARCHIVED, сегодня вообще не имеет UI-пути обратно вACTIVE— только через API в обе стороны. - Force owner transfer (
POST :orgId/force-owner-transfer, «Take ownership» на странице карточки) —SuperAdminGuard, а не общее право:manage. Это явно «путь восстановления SUPER_ADMIN» (согласно комментарию в коде) для случая, когда организация застряла без доступного OWNER — обладательplatform:organizations:manage, не являющийсяSUPER_ADMIN, не может им воспользоваться. - Принудительная установка языка по умолчанию (
PATCH :orgId/default-locale, добавлено 2026-08-18) — та же схема, что и force owner transfer:SuperAdminGuard, а не:manage, и без проверки членства. Переопределяет собственную настройку организации языка по умолчанию, доступную только OWNER, снаружи организации.
Диалог «New workspace» на странице списка создаёт организацию напрямую со стороны платформы (тот же POST /organizations, что и при самостоятельном создании — см. IAM); его кнопка закрыта правом :manage, хотя сама страница требует только :read.
Исправлено, 2026-08-04: этот диалог раньше падал с 500 при каждой отправке — организация реально создавалась в базе (транзакция коммитится раньше, чем строится HTTP-ответ), но OrganizationsService.create() возвращал сырую строку Prisma вместо маппинга в OrganizationDto, а Organization.storageQuotaBytes/storageUsedBytes (BigInt-колонки, добавленные для docs/storage/ADR-001-file-storage.md) обрушивают JSON.stringify — так что админ видел ошибку и не мог узнать, что организация существует. getById() уже избегал этого, вручную формируя возвращаемое значение; теперь create() делает то же самое. Найдено через scripts/e2e/menu/platform-admin.mjs, который реально прогоняет этот диалог при каждом запуске.
Пользователи (/platform/users, /platform/users/[id])
Простыми словами: каждый человек с аккаунтом P4P, по всем организациям. Сегодня интерфейс позволяет только удалить аккаунт целиком — временно приостановить/заблокировать нужно напрямую через API (кнопки пока нет).
admin-users.controller.ts, смонтирован на admin/users.
- Список / список platform-staff / список platform-staff-invitations / детали (
GET) —platform:users:read. - Удаление аккаунта пользователя (
DELETE :id) —platform:users:manage, единственное действие со статусом аккаунта, которое реально есть в UI этой страницы (действие по строке на/platform/users). - Suspend / activate / lock / unlock (
POST :id/suspend|activate|lock|unlock) — подтверждено (2026-08-04, поиск поapps/portal): точки входа в интерфейсе нет ни для одного из четырёх, хотя все четыре полностью реализованы и закрыты правами на бэкенде. Если сегодня нужно приостановить или разблокировать аккаунт — это прямой API-вызов, кнопки нет. - Impersonate — действия «Impersonate» на этой странице не существует. Подтверждено 2026-08-04: Impersonation полностью серверная функция (
POST /auth/impersonate, толькоSUPER_ADMIN) — на этой странице нет подключённой к ней кнопки, хотя это естественное для неё место. - Убрать из platform staff (
DELETE :id/platform-staff) —platform:users:manage. ЗакрываетPlatformStaffMembershipчеловека и все назначенияPlatformRole; отличается от полной приостановки его аккаунтаUser. - Effective permissions (действующие права, 2026-08-14) — панель «Platform roles» на странице деталей пользователя показывает живую, только для чтения, сводку объединения прав всех отмеченных ролей, сгруппированную по областям в свёрнутом по умолчанию аккордеоне, пересчитываемую мгновенно при отметке/снятии ролей и до нажатия Сохранить роли — предпросмотр того, что реально даст изменение, а не отчёт о том, что уже сохранено. Тот же компонент-аккордеон обслуживает и собственное чтение сохранённых прав самим пользователем на
/account/profile— различаются только источник данных и текст. - Leave / return from leave (
POST :id/leave,POST :id/return-from-leave) —platform:users:manage. Статус внутреннего штата (в отпуске/активен), не приостановка аккаунта — влияет на доступность для назначения на задачи/проекты в модуле Персонал, не на возможность входа.
Приглашения в штат платформы (/platform-staff-invitations/[token])
Простыми словами: как кто-то присоединяется к собственной внутренней команде P4P (в отличие от членства в организации клиента). Отправляется с /platform/staff — отдельный процесс от приглашения кого-то в рабочее пространство компании.
platform-staff-invitations.controller.ts. Отправка приглашения (POST /platform-staff-invitations) закрыта обычным PlatformRoleGuard + platform:users:invite (делегировано 2026-08-13 — см. гайд по полям выше) — не SuperAdminGuard, гвардом, используемым для impersonation и force-owner-transfer. SUPER_ADMIN и тот, кто занимает платформенную роль P4P Owner, всегда получают это право посевом по умолчанию, намеренно: приглашение кого-то в штат платформы — это обычная работа по укомплектованию штата, которую Owner компании должен уметь делать напрямую, в отличие от действий восстановления безопасности, которые остаются только для SUPER_ADMIN. Приглашение никогда не может нести грант роли SUPER_ADMIN (проверяется на сервере, даже несмотря на то что сама отправка уже закрыта). Принятие (GET/POST :token[/accept], публично) создаёт запись PlatformStaffMembership — предпосылку для того, чтобы вообще занимать любую PlatformRole (см. Глоссарий).
Страница принятия (apps/portal/pages/platform-staff-invitations/[token].vue) почти зеркально повторяет форму /invitations/[token] — тот же guard isWrongAccount, тот же принцип «принятие в один клик, если уже авторизован», та же развилка регистрация-vs-вход — но намеренно проще, так как это небольшой, редко используемый внутренний процесс (docs/ROADMAP.md → Internal Company Workspace): нет magic link, нет OTP, нет кнопки Google, и — единственное структурное отличие, которое стоит отдельно отметить, не очевидное при беглом чтении — вообще нет нейтральной кнопки-шлюза «Accept invitation». /invitations/[token] сначала показывает простую кнопку-шлюз и только после клика раскрывает форму входа/регистрации; у этой страницы нет started-ref вообще, поэтому неавторизованный посетитель сразу попадает на нужную форму. Аккаунт без пароля (только Google) просто получает инструкцию войти где-то ещё и вернуться, а не встроенный сброс пароля, как на странице приглашения в организацию.
Продукты и подписки (/platform/products)
Простыми словами: каталог продуктов, на которые могут быть подписаны организации, и кто на что подписан. Сами продукты добавляют инженеры, не с этой страницы — эта страница только редактирует, как существующие продукты отображаются, и выдаёт/отзывает доступ организации к ним.
admin-products.controller.ts, смонтирован на admin (роуты: products, products/:productId, organizations/:orgId/subscriptions, subscriptions/:subscriptionId).
- Список каталога продуктов —
platform:products:read; создание/редактирование/деактивация —platform:products:manage. Меню редактирования/активации на странице видно при:read, но доступно для использования только при:manage. - Подписки организации — список требует
platform:subscriptions:read, выдача/отзыв —platform:subscriptions:manage. Самостоятельного сценария подписки сегодня нет — организация не может подписаться на продукт сама; это делает платформенный админ.
Роли (/platform/roles)
Простыми словами: та же идея, что и Roles организации (IAM), но для собственной внутренней команды P4P, а не команды клиента — отдельный набор ролей и прав, никогда не сливающийся с ролями организации.
platform-roles.controller.ts, смонтирован на admin/platform-roles. Та же форма, что и у страницы Roles уровня организации (IAM), но для PlatformRole/PlatformPermission вместо Role/Permission — структурно отдельная пара таблиц, и PermissionsGuard/TenantGuard (путь применения прав уровня организации) никогда не читает грант PlatformPermission, даже для SUPER_ADMIN (docs/iam/PERMISSIONS_CATALOG.md).
Удобство выдачи, добавлено 2026-08-19 — linkedOrgRoleId. Платформенная роль может опционально указывать одну роль (Role) P4P Internal (например, «Team Manager»), которая также выдаётся при назначении этой платформенной роли, одним действием — задаётся в гайде по полям. Это не нарушает разделение выше: ничто в PermissionsGuard не читает права платформенной роли, и ничто в PlatformRoleGuard не читает права роли организации — собственный поток назначения платформенной роли (PlatformRolesService.assignToUser) просто дополнительно создаёт/обновляет строку MembershipRole в P4P Internal как вторую, независимую запись. Отзыв убирает именно этот грант, кроме случая, когда это оставило бы членство вообще без ролей (тихо пропускается, вместо того чтобы проваливать отзыв платформенной роли из-за состояния несвязанного членства). Ограничено собственными ролями P4P Internal — привязка к роли другой организации была бы бессмысленной, поскольку платформенный штат автоматически зачисляется только в P4P Internal.
- Список ролей + список каталога прав —
platform:roles:read. - Передача посеянной роли
SUPER_ADMIN/эквивалента владельца (PATCH owner/transfer) — намеренно без декоратора@RequirePlatformPermission(см. комментарий в коде на строке 49): собственная логикаPlatformRoleGuardдля этого роута обеспечивает что-то более строгое, чем может выразить система прав. Не считай, что «нет декоратора» значит «нет защиты». - Создание / редактирование / удаление кастомной платформенной роли, назначение/снятие роли пользователю, добавление/удаление права у роли — всё требует
platform:roles:manage. Как описано в связанных заметках каталога в Администрирование платформы → Пользователи: обладательplatform:roles:manageвсё ещё может напрямую выдатьSUPER_ADMINуже активному сотруднику платформенного штата черезPOST :roleId/users/:userIdбез дополнительной защиты — известный, сейчас принятый пробел (см.docs/iam/PERMISSIONS_CATALOG.md, «Still open»).
Посеянные роли: SUPER_ADMIN (системная, полный обход), P4P Owner и P4P Super Admin (явно получают полный каталог прав, для отображения в UI — см. Глоссарий), P4P Admin (отобранное подмножество, исключает platform:roles:manage). P4P Member удалена, 2026-08-19 — её набор прав был сведён к нулю ещё в тот же день, и при проверке оказалось, что она больше нигде не была значимой (единственное место, которое когда-либо привязывало реальный доступ к «удержанию хоть какой-то платформенной роли», к тому времени уже было мёртвым, неиспользуемым кодом). Новый приглашённый в платформенный штат без выбранных ролей теперь просто не имеет ни одной PlatformRole — реальный доступ к Team Management всегда приходит из отдельной синхронизации PlatformStaffMembership → членство в P4P Internal, а не из платформенной роли. Полное обоснование — в собственной датированной записи docs/iam/PERMISSIONS_CATALOG.md.
Журнал аудита (/platform/audit)
Простыми словами: просмотрщик, только для чтения, той же платформенной истории, что описана в IAM → Журнал аудита. Намеренно отделён от «P4P Admin», повседневной админской роли — чтение истории активности всех считается более чувствительным, чем обычная админская работа.
admin-audit.controller.ts, смонтирован на admin/audit-log, platform:audit:read. Просмотр только для чтения над AuditLog, доступным только на добавление (записывается со всей платформы, не только этим модулем — см. IAM → Журнал аудита). Намеренно исключён из удобной роли P4P Admin — «кто следит за следящими» уже, чем обычная админская работа, то же рассуждение, что держит platform:roles:manage вне этой роли.
Анонсы (/platform/announcements)
Простыми словами: способ для супер-админа сказать каждому сотруднику P4P «вот что нового» — и в колокольчике уведомлений, и по почте — без привязки к деплою как триггеру. Полное обоснование — в датированной записи docs/iam/PERMISSIONS_CATALOG.md от 2026-08-24.
announcements.controller.ts, смонтирован на admin/announcements, platform:announcements:manage (только супер-админ — намеренно исключён из удобной роли P4P Admin, то же рассуждение «кто следит за следящими», что и у platform:roles:manage/ platform:audit:read выше). Список получателей строится точно так же, как для любой другой рассылки Team Management — каждый обладатель workspace:staffing:read в P4P Internal, а не новый механизм «все пользователи платформы» — такого нет. Доставляется через обычный конвейер уведомлений (docs/notifications/ADR-001-notifications.md) под собственной группой настроек announcements, так что получатель, которому это не нужно, может отключить группу, как и любую другую — всё, кроме account_security, опционально. title/body уходят дословно и как текст в колокольчике, и как тема/тело письма — в отличие от любого другого вида уведомления, это не значения для подстановки в фиксированное предложение, это и есть само сообщение.
Намеренно не привязан ни к деплоям, ни к собственному номеру версии из package.json (тот показывается отдельно, в футере сайдбара портала) — не каждый деплой стоит анонсировать, и написать «вот что изменилось» словами, которые реально хочет прочитать читатель, может только человек. Бамп версии в package.json — отдельное, полностью ручное git-изменение; отправка анонса — естественный повод сделать это заодно, но автоматически здесь ничего не происходит.
AI-ассистенты (/platform/ai-assistants)
Простыми словами: ассистент, с которым разговаривает каждое пространство, пока у него нет своих агентов, и единственное место, где его можно настроить.
Это одна запись Agent с organizationId: null в базе intelligence — одна общая запись, а не копия на организацию. Поэтому её не может править ни одно пространство (AgentsService.findOwned отказывает при organizationId: null, отвечая 404) и поэтому для правки нужно платформенное право, а не организационное ai:agents:manage. Каждое пространство по-прежнему видит его только для чтения на странице агента в AI Studio, а обладатель права получает оттуда ссылку Настроить сюда.
platform-agents.controller.ts в intelligence, смонтирован на platform/agents. С 17.09.2026 права два: маршруты на чтение спрашивают platform:ai_assistants:read, который есть у P4P Admin, а маршрут на запись — platform:ai_assistants:manage, остающийся на Super Admin / Owner. Та же логика «затрагивает всех», что и у анонсов, но применённая к инструкциям, а не ко всей странице: тому, кто разбирает «ассистент перестал отвечать», нужно видеть, какая за ним модель, — и раньше это стоило ему права на запись. Страница открывается в режиме чтения и гасит поля, а не прячет их. Он стоит за PlatformAccessGuard, а не за OrgAccessGuard, через который идут все остальные маршруты intelligence: тот требует заголовок X-Organization-Id и отвечает правами одной организации, что для записи, не принадлежащей ни одной, ровно неверно. Guard намеренно не проставляет организацию и в сам запрос — глобальная запись, тихо привязанная к тому пространству, которое было открыто у администратора, и есть та ошибка, ради предотвращения которой это разделение существует.
Меняются: название, описание, инструкции, модель. Больше ничего — см. что изменить нельзя выше.
Изменения записываются как ai_assistant.updated в журнал аудита. Запись подаётся в core-api из intelligence от имени администратора — так же, как уведомление о завершении запуска агента: журнал принадлежит core-api, и intelligence не заводит второй. В отличие от тех best-effort записей эта падает громко, и изменение откатывается вместе с ней: если его не удалось записать, оно не сохраняется. Изменение записи, которой пользуются все организации, за которое потом никто не отвечает, хуже несостоявшегося изменения.
Сид больше его не перезаписывает. Контейнер intelligence запускает сид базы при каждом старте, и до 10.09.2026 сид каждый раз переписывал инструкции и модель ассистента — то есть молча откатывал любую правку на следующем деплое. Теперь он только создаёт ассистента, если того нет. Чтобы осознанно заменить то, что лежит в базе, новым поставляемым промптом или моделью по умолчанию, поставь PLATFORM_ASSISTANT_RESEED=1 на один деплой и снова убери (см. docs/DEPLOYMENT.md).
AI-модели (/platform/ai-models)
Простыми словами: единственный список моделей, из которого выбирают агенты всех организаций, и единственное место, где его можно править.
Строки ModelConfig в базе intelligence глобальны — один общий каталог, а не копия на каждую организацию. Чтение открыто любому вошедшему: форма агента обязана предлагать список всем, кто настраивает агентов, а названия моделей не чувствительны. Запись живёт в platform-model-configs.controller.ts на platform/model-configs за PlatformAccessGuard и platform:ai_models:manage — собственный ключ с 17.09.2026, и он есть у P4P Admin. До этого ключ был общий с AI-ассистентами, по логике «настраивающий ассистента и чинящий стоящую за ним модель — один и тот же человек». Оказалось, что это разные люди: вести каталог — повседневная поддержка, а переписывать то, что ассистент говорит всем пространствам, — уровнем выше.
Ключ, на котором всё работает, задаётся вверху этой страницы, а не в файле. До 17.09.2026 он жил только в OPENROUTER_API_KEY, поэтому смена ключа означала ssh и перезапуск. Теперь он хранится зашифрованным и задаётся здесь, за правом platform:ai_key:manage — только Super Admin и Owner, потому что это платёжный доступ за всю установку, и на нём работают все пространства без своего ключа. Переменная окружения — только семя: читается один раз при первом запуске, если ключа ещё нет, и больше никогда, так что убранный здесь ключ остаётся убранным. Сам ключ обратно не показывается — карточка сообщает только, задан ли он и когда менялся.
Каждое изменение здесь пишется в журнал аудита как ai_model.created, ai_model.updated или ai_model.withdrawn — с фильтром AI-модели. Появилось вместе с ключом и по той же причине: если модель убрать, каждый выбравший её агент падает на следующем запуске, а теперь это повседневная работа, а не действие Super Admin'а, — значит, на вопрос «кто убрал» должен быть ответ. Если запись в журнал не проходит, изменение не остаётся: оно откатывается, и экран сообщает об ошибке, вместо того чтобы оставить изменение, за которое некому отчитаться.
До 15.09.2026 эта запись была закрыта орг-правом ai:agents:manage, то есть админ любой организации мог переназначить или убрать модель, которой пользовались все остальные.
Выбор модели. Диалог читает собственный список моделей OpenRouter, причём читает сервер (GET /platform/model-catalogue, кэш на десять минут), а не браузер: openrouter.ai отвечает 403 из некоторых регионов, откуда платформой управляют, а Content-Security-Policy портала его не называет. Тот же список проверяется при сохранении — идентификатор OpenRouter, которого в нём нет, отклоняется. Именно это остановило бы qwen3.8-27b:free, (без префикса производителя, с запятой в конце), сохранённый 21.09.2026. Строку, чей идентификатор уже снят, по-прежнему можно отключить или переименовать: проверяется только изменённый идентификатор. Если список прочитать не удалось совсем, поле становится текстовым и сохранение проходит без проверки: «неизвестно» — не то же самое, что «нельзя», а установка без интернета всё равно должна уметь записать модель. У остальных провайдеров нет списка, который платформа могла бы прочитать (их ключи принадлежат пространствам), поэтому их идентификаторы вводятся вручную. Сам провайдер — одно из четырёх значений (openrouter, anthropic, openai, google), потому что он определяет, через какой API вызывается модель и чьим ключом.
Зачем вообще этот экран. Поставщик может убрать идентификатор модели когда угодно, и убранный идентификатор не даёт ошибки — запрос проходит и возвращает пустоту. Все ассистенты платформы замолкают, и ни один лог не объясняет почему. Так и случилось 15.09.2026 с бесплатной моделью, засеянной месяцами ранее, и единственным лекарством была правка посева с деплоем.
Чего в форме нет: подмены базового адреса. Она решает, куда уходит общий ключ платформы, и задаётся только в базе. См. docs/ai/ADR-001-ai-platform-architecture.md, Решение 14.
Тестирование этого модуля
scripts/e2e/menu/platform-admin.mjs (23 проверки) прогоняет основные сценарии этого модуля от начала до конца и проставляет lastVerified выше, плюс по юнит-тест-файлу на страницу (apps/portal/pages/platform/*.test.ts) по мере их добавления — постранично, отдельным раундовым разделом усилий:
- Dashboard —
apps/portal/pages/platform/dashboard.test.ts, 6 тестов — покрывает, какие карточки/запросы хаба закрыты каким read-правом, реальное разбиение «всего/подмножества» по каждой карточке, то, что панели «Последняя активность» нужны ОБА условия —platform:audit:readИ хотя бы одна запись, и что немаппленный код действия аудита отображается своим сырым кодом, а не пустой меткой;platform-admin.mjsтакже подтверждает, что/platformреально ведёт на/platform/dashboardс реальными числами, а не просто что URL содержит «/platform». - Organizations —
apps/portal/pages/platform/organizations/{index,[id]}.test.ts, 16 тестов — фильтры поиска/типа/статуса/членства/владельца, собственная проверка членства значка «свой», поведение автогенерации slug из name до первого ручного редактирования и успешный/неуспешный пути создания с перезагрузкой списка и обновлением useCurrentUser, собственное правило доступности «Take ownership» (только SUPER_ADMIN, не для себя, только на строке другого OWNER) и его двухшаговый гейт «введите точное имя», и ветвление POST/PATCH при переключении подписки на продукт (REACTIVATABLE_STATUSES);platform-admin.mjsдополнен живой выдачей подписки со страницы деталей организации — первое живое упражнение подписок во всём этом наборе e2e. - Users —
apps/portal/pages/platform/users/{index,[id]}.test.ts, 18 тестов — фильтры поиска/статуса/роли/наличия рабочих пространств, опции фильтра ролей, выводимые из ролей загруженных пользователей (дедуп и сортировка), защита от действия над собственной строкой в меню действий (поверхplatform:users:manage), успешный и неуспешный пути подтверждения удаления, собственное правило доступности «Remove from platform staff» (SUPER_ADMIN или Owner, цель действительно должна быть platform staff), правило кнопки передачи владения (только текущий Owner, цель — platform staff, ещё не имеющий P4P Owner) и поведение диалога при успехе/неудаче, список доступных для назначения ролей, всегда исключающий P4P Owner и исключающий P4P Super Admin если вы не Owner, и собственное дифф-сравнениеsaveRolesпри назначении/отзыве (запрос уходит только по реально изменившимся ролям);platform-admin.mjsдополнен живым циклом «назначить роль → отозвать platform staff» на аккаунте, который уже создаютtestStaff/acceptStaffInvite— собственные изменяющие действия страницы деталей не имели живого покрытия до этого раунда. Намеренно НЕ прогоняет «Transfer ownership» живьём — не баг, а осознанно пропущенный деструктивный путь (передача собственной роли Owner у посевного SUPER_ADMIN одноразовому аккаунту без автоматизированного пути назад). - Roles + Products —
apps/portal/pages/platform/{roles,products}/index.test.ts, 18 тестов — сортировка «сначала системные, затем кастомные по алфавиту», сворачивание бейджей прав «топ-3, затем "+N ещё"», доступность Duplicate даже на системной роли, при том что Edit/Delete требуют несистемной роли, работа диалога просмотра прав, поиск по продуктам по name/code/description, право доступа к меню действий, предзаполнение диалога редактирования из текущей строки, и успешный/неуспешный PATCH как для формы редактирования, так и для переключателя активации;platform-admin.mjsдополнен живым циклом «деактивировать → снова активировать» на странице Products — собственный реальный операционный рычаг этой страницы (по её же комментарию в коде), не имевший живого покрытия до этого раунда, восстанавливается обратно в Active, чтобы реальный посевной платформенный продукт не остался выключенным. «Duplicate» сама по себе не гоняется живьём — она отправляет запрос на тот же самый эндпоинт, который уже проверяет существующее создание кастомной роли, просто с иначе предзаполненной формой. - Staff —
apps/portal/pages/platform/staff/index.test.ts, 7 тестов — фильтры поиска/статуса/роли, опции фильтра ролей, выводимые из ролей загруженного персонала, и то, что «Invite to platform staff» требует именноplatform:users:invite(а не общееplatform:users:manage). Написано, когда этот гейт был ещё жёстко закодированной паройisSuperAdmin || isOwner/SuperAdminOrOwnerGuard— делегировано обычному праву 2026-08-13 (см. Приглашения в штат выше); уSUPER_ADMIN/P4P Ownerоно по-прежнему есть по умолчанию, просто больше не как жёстко закодированный частный случай.platform-admin.mjsуже покрывал приглашение + принятие живьём в более раннем раунде — новых e2e здесь не понадобилось. - Принятие приглашения в штат платформы (
/platform-staff-invitations/[token]) —apps/portal/pages/platform-staff-invitations/[token].test.ts, 11 тестов — ошибка загрузки недействительного/истёкшего приглашения, текст с ролями vs. без ролей, несовпадение аккаунта + выход, принятие в один клик при успехе/ошибке для уже авторизованного правильного аккаунта, вход по паролю + принятие (включая специфичное для этого приглашения сообщение «неверный пароль»), простая инструкция «войдите где-то ещё» для аккаунта без пароля (поле пароля вообще не рендерится — намеренно более простой поток, чем встроенный сброс пароля на странице приглашения в организацию, см. Принятие приглашения), и форма регистрации по умолчанию с успехом/ошибкой отправки. Новый e2e-файлscripts/e2e/menu/platform-staff-invitation-accept.mjs(6 проверок, 2026-08-12) покрывает три ветки, которые не затрагивают собственныеtestStaff/acceptStaffInviteизplatform-admin.mjs(та пара всегда идёт только по пути регистрации нового пользователя): принятие в один клик уже авторизованным аккаунтом, шлюз несовпадения аккаунта и вход по паролю для существующего, но не авторизованного аккаунта. При написании этого файла найдено реальное структурное отличие от страницы приглашения в организацию: у этой страницы вообще нет нейтральной кнопки-шлюза «Accept invitation» — неавторизованный посетитель сразу попадает на нужную форму, без промежуточного клика. - Audit log —
apps/portal/pages/platform/audit/index.test.ts, 11 тестов — дефолтные параметры запроса (limit/offset/order: desc), собственные параметры перезапроса каждого фильтра, включая валидацию диапазона дат from/until, блокирующую плохой запрос ещё до отправки, математику пагинации offset/limit и поведение сброса на страницу 1, и реальные правила разбораauditChanges/auditExtraMetadata/auditValueLabel(реальный diffmetadata.changesимеет приоритет над сырым дампом ключ/значение всего остального вmetadata), и рендер разрешённых имён исполнителя/объекта с кнопкой копирования у каждого id — с откатом на сырой тип + UUID, когда бэкенд не разрешил имя.platform-admin.mjsуже покрывал рендер записей живьём в более раннем раунде.
Это закрывает юнит-покрытие для каждой страницы под /platform/*, которую документирует этот модуль. platform/notifications.vue (маршрутизируется под /platform/, но документируется в Notifications, не здесь) получил свой раунд в этом же усилии — включая реальный, значимый найденный баг — см. собственный раздел «Тестирование этого модуля» этого модуля; 23-проверочный итог platform-admin.mjs включает и его 5 проверок тоже.
Найдено, не исправлено (код приложения, вне рамок этого раунда тестирования — полную историю см. в собственном описании scripts/e2e/README.md): значок членства «свой» в списке организаций и собственная замыкающая ссылка DataTable на деталь имеют буквально одинаковое доступное имя, «Open workspace» (platform.organizations.openWorkspace и common.openWorkspace — два разных ключа i18n, которые случайно резолвятся в идентичный английский текст), но ведут в два разных места (вход в рабочее пространство как участник vs открытие вида платформенного админа). Проявляется только на строке, где просматривающий SUPER_ADMIN сам оказывается участником — верно для любой организации, которую он только что создал сам, поскольку создатель становится её OWNER.
Найдено, не исправлено (5-й случай известного бага, впервые найден в 2026-07 на собственном диалоге удаления team/departments/[id]/roles.vue — полную историю см. в описании этого раунда в scripts/e2e/README.md и остальных 3 случаях): диалог подтверждения удаления в platform/users/index.vue использует AlertDialogAction, у которой есть собственный внутренний обработчик закрытия по клику, который reka-ui сливает с @click="confirmDelete" этого приложения на той же кнопке — реальный клик во время НЕУДАЧНОГО удаления закрывает диалог немедленно независимо от асинхронного результата, поэтому встроенная ошибка так и не отображается (тост при этом всё равно показывается, поскольку он не привязан к состоянию диалога).
Найдено, не исправлено (6-й случай того же бага): собственный диалог подтверждения удаления в platform/roles/index.vue имеет ту же форму — тот же баг, тот же отложенный фикс по той же причине.
Не покрыто автоматизацией, стоит проверить руками — см. docs/MANUAL_TESTING.md: закрытие read/manage правами изменяющих элементов на каждой странице, блокировка роли SUPER_ADMIN в приглашении в штат, путь передачи владельческой платформенной роли и закрытие platform:audit:read на самой странице Audit Log.