01. Руководство platform_admin
Аудитория. Владелец платформы / инфраструктурный администратор. Единственная роль с флагом is_platform_admin = TRUE в таблице users. Обходит RLS через PostgreSQL-роль lms_system (при обращениях через привилегированный движок).
Что может platform_admin
- Видеть все школы, всех пользователей, все курсы, все платежи.
- Создавать новые школы и их первых
school_admin. - Отключать/удалять школы (через миграции / БД).
- Настраивать глобальные Payment Providers (stripe, vipps, paypal, mock).
- Наблюдать совокупные LLM-costs по всем школам.
- Запускать миграции и бэкапы БД (кросс с 05-developer.md).
- Делать
deep_copyкурса от имени любой школы (черезX-School-Id).
Что не делает напрямую через UI:
- Не пишет курсы (это работа teacher'ов конкретной школы).
- Не общается со студентами.
Доступ
Учётка в 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-Id → 400 (см. 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}.
Ежедневные обязанности
- Мониторинг cost-дашборда — чтобы никто случайно не ушёл в бюджет.
- Проверка
/dashboard→ CostReport по школам-аутлаерам. - Проверка Temporal UI http://127.0.0.1:55480 — нет ли залипших workflows.
- Ротация бэкапов БД — dev-backups в MinIO (
s3://db-backups/), проверять что скриптscripts/backup_db_to_minio.shотрабатывает перед каждой миграцией.
Чего НЕ нужно делать
- Не править
agent_promptsнапрямую UPDATE/DELETE — таблица append-only, триггерagent_prompts_no_updateблокирует. Новая версия = новая строка черезscripts/prompt_cli.py publishили миграция (ADR-0011). - Не коммитить
.env.local,/memories/,.vscode/mcp.json— в.gitignore. - Не запускать миграции без предварительного дампа в MinIO (
scripts/backup_db_to_minio.sh <tag>). - Не хардкодить строки Gemini-моделей — только через
ModelTierenum.
Куда смотреть при инцидентах
| Симптом | Куда |
|---|---|
| Сервис лежит | tail -f /tmp/lms-api.log |
| Workflow не завершается | http://127.0.0.1:55480 (Temporal UI) |
| Бюджет кончился | CostReport + .env.local → MAX_DAILY_USD |
| БД сломалась | последний бэкап s3://db-backups/... |
| RLS пропускает чужое | проверить SET LOCAL app.current_user_id в роутере, см. ADR-0001 |