01-platform-admin

01. Руководство platform_admin

Аудитория. Владелец платформы / инфраструктурный администратор. Единственная роль с флагом is_platform_admin = TRUE в таблице users. Обходит RLS через PostgreSQL-роль lms_system (при обращениях через привилегированный движок).


Что может platform_admin

Что не делает напрямую через UI:


Доступ

Учётка в dev:
admin@local / change_me_locally

Фронт: http://127.0.0.1:3500/login → Sign in → /dashboard.

На /dashboard признак «platform admin» отрисован в заголовке (pill-лейбл).


Типовые задачи

Посмотреть все школы

Через UI: /dashboard → секция «Visible schools (RLS-scoped)» — для platform_admin'а RLS-фильтр возвращает все школы.

Через API:

curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:55400/demo/visible-schools

Создать новую школу

Через UI пока нет формы — делается SQL-скриптом или ad-hoc-миграцией (скоро — UI). Минимально:

INSERT INTO schools (id, slug, name, created_at)
VALUES (gen_random_uuid(), 'acme-academy', 'Acme Academy', NOW());

-- Дать кому-то роль school_admin в этой школе:
INSERT INTO user_memberships (user_id, school_id, role_code)
VALUES ('<user-uuid>', '<school-uuid>', 'school_admin');

Никогда не делайте это на прод-БД без бэкапа. См. 05-developer.md § Миграции.

Посмотреть LLM-затраты всех школ

/dashboard → секция «LLM cost (last 30 days)» → компонент CostReport.

Под капотом — агрегация llm_cost_entries по всем школам (RLS пропускает platform_admin).

API:

curl -H "Authorization: Bearer $TOKEN" \
  'http://127.0.0.1:55400/cost/report?days=30'

Soft-cap по бюджету: MAX_DAILY_USD=5.0 в .env.local (для dev). В проде настраивается на уровне провайдера/аккаунта Google.

Сделать deep_copy курса от имени школы

Platform_admin не привязан к школе автоматически — нужно указать X-School-Id:

curl -X POST "http://127.0.0.1:55400/courses/<course_id>/copy" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-School-Id: <school_uuid>" \
  -H "Content-Type: application/json" \
  -d '{"new_course_id":"copy-001","new_title":"Platform clone"}'

Без X-School-Id400 (см. manifest §14 про deep_copy).

Провайдеры платежей

Список провайдеров:

curl http://127.0.0.1:55400/billing/providers

Переключение активного провайдера конкретной школы — через таблицу payment_providers (пока SQL). Поддерживаемые типы: stripe, vipps, paypal, mock. Все 4 в dev — это моки (см. ADR-0013).

Webhook-эндпоинты провайдеров

Каждый провайдер имеет унифицированный endpoint:

POST /billing/webhooks/{provider}

Не требует auth (подпись проверяется на app-уровне), использует superuser-движок → обходит RLS. Идемпотентно логируется в payment_events. Replay возвращает {status:"duplicate", ack:true}.


Ежедневные обязанности

  1. Мониторинг cost-дашборда — чтобы никто случайно не ушёл в бюджет.
  2. Проверка /dashboard → CostReport по школам-аутлаерам.
  3. Проверка Temporal UI http://127.0.0.1:55480 — нет ли залипших workflows.
  4. Ротация бэкапов БД — dev-backups в MinIO (s3://db-backups/), проверять что скрипт scripts/backup_db_to_minio.sh отрабатывает перед каждой миграцией.

Чего НЕ нужно делать


Куда смотреть при инцидентах

Симптом Куда
Сервис лежит tail -f /tmp/lms-api.log
Workflow не завершается http://127.0.0.1:55480 (Temporal UI)
Бюджет кончился CostReport + .env.localMAX_DAILY_USD
БД сломалась последний бэкап s3://db-backups/...
RLS пропускает чужое проверить SET LOCAL app.current_user_id в роутере, см. ADR-0001