Files
portal/docs/Runbook_ekspluatatsii_v8_2.md
T
Дмитрий 887abf444e rebrand(v8.5→Лидерра): дизайн-handoff Платона v8 Forest + Лидпоток→Лидерра
Получен handoff-пакет liderra_v8_handoff/ от дизайнера Платона
(kpd9363@gmail.com) от 07.05.2026 — v8 Forest. Заказчик 08.05 решил
применить только в части дизайна, имени, логотипа. Функционал, состав
страниц и правила (CTO-11, click-wrap, SSO break-glass, 14 статусов
воронки) — без изменений (источник — ТЗ v8.5/schema v8.5).

Что сделано:

- Массовая замена Лидпоток→Лидерра (с учётом падежей: Лидерры/Лидерре)
  в 33 файлах (449 вхождений) — все .md/.sql/.json/.toml/.yml/.txt/.html,
  кроме исторических упоминаний внутри liderra_v8_handoff/
- Удалён docs/brandbook.md v1.1 — заменён на BRANDBOOK_v2.md из handoff
- Скопированы 13 концептов liderra_v8_handoff/concepts/v8_*.html в
  web/v8/. Удалены старые web/01-login.html, 02-dashboard.html,
  03-deals.html, index.html (палитра v1.1 deprecated)
- CLAUDE.md v1.0→v1.1: §0 (BRANDBOOK_v2 + DEVELOPER_HANDOFF в источниках),
  §2 (палитра Forest, Inter+JBM, Lucide), §5 п.6 (anti-pattern Inter
  снят — в Forest Inter наш основной шрифт), §6 (13 концептов в web/v8/)
- Реестр Открытые_вопросы_v8_3.md v1.12→v1.13: добавлена запись о
  ребрендинге + 4 точечных расхождений handoff vs ТЗ (статусы воронки,
  click-wrap чекбоксы, SSO fallback, axe violations)
- package.json/package-lock.json: name lidpotok→liderra

4 расхождения handoff vs ТЗ (НЕ применены, источник истины — ТЗ/schema):

1. 14 «обобщённых» статусов в BRANDBOOK_v2 §3.6 ≠ 14 slug'ов в
   schema.sql:2076 (совпадает 2 из 14: «Переговоры», «Оплачено»).
   Источник — schema/ТЗ §6.4 (реселлерская модель из аудита crm.bp-gr.ru,
   6 системных + 8 настраиваемых статусов).
2. 3-й click-wrap в v8_login.html («маркетинг-опционально») ≠ ТЗ §1.5/§4.1
   («согласие на ПДн», обязательное, OPEN-Ж-3).
3. SSO в v8_admin.html («локальный 2FA fallback») ≠ ТЗ OPEN-И-13
   (break-glass super_admin, локальный 2FA выключен).
4. Заявление «axe-core 4.10.2 — 0 violations» в README handoff — локально
   Pa11y 9.1.1 + axe нашёл 81 violation на 10/13 HTML (преимущественно
   color-contrast на декоративных separator'ах с --ink-disabled).
   Чисто: settings/errors/palette_options.

Что НЕ включено в коммит:
- лендинг/TZ_landing_v1_0.md — untracked, не моя работа в этой сессии
- .tmp/ — gitignored

Что осталось (для следующих сессий):
- Возможное переименование GitHub-репо CoralMinister/lidpotok → liderra
  (отдельное решение заказчика)
- Опционально: обратная связь Платону по 4 расхождениям handoff vs ТЗ

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:11:58 +03:00

61 KiB
Raw Blame History

Runbook эксплуатации — v8.2, Приложение И

Версия: 8.2 от 04.05.2026 (правки после интервью с заказчиком).

Что нового в v8.2 относительно v8.1

  • OPEN-И-1 закрыт. Таблица incidents_log создана (DDL — в Прил. Д v8.2 «Что нового»). Все типовые инциденты этого runbook должны теперь записываться в incidents_log через INSERT или через хелпер IncidentLogger::open(type, severity, summary) в коде. Формат: started_at = первая зафиксированная аномалия, detected_at = время алерта или ручного обнаружения, resolved_at = NULL до закрытия инцидента, потом проставляется. После закрытия — обязательно заполнить root_cause и postmortem_url.
  • OPEN-И-2 закрыт. Стратегия CRM-интеграций:
    • На MVP: только outbound webhook (webhook_endpoints таблица, базовая отправка с retry — уже в плане).
    • Архитектурный задел: интерфейс CrmConnectorInterface в коде, пустая таблица crm_connections (поля: id, tenant_id, provider, oauth_token, settings JSONB, last_sync_at, status, created_at). Без UI.
    • Спринт 1415 (пост-MVP-frontend, перед public release): первый коннектор — amoCRM. OAuth-флоу, mapping полей, обработка лимитов API amoCRM (7 req/sec), retry с exponential backoff, тесты на sandbox.
    • Post-MVP по запросу клиентов: Bitrix24, RetailCRM.
  • 🔧 OPEN-И-3..10, OPEN-И-12 продолжают ждать: контакты on-call (OPEN-И-12, P1), шаблоны постмортемов, escalation timing — после Б-1 + DO-4.

Изменения 05.05.2026 (v8.3.1): разрешена коллизия OPEN-И-2. Запись «контакты эскалации» (была OPEN-И-2 в v8.1) переименована в OPEN-И-12 (см. таблицу В.1). Текущий смысл OPEN-И-2 — стратегия CRM-интеграций ( закрыт). OPEN-И-11 — миграции crm_connections/crm_field_mappings ( закрыт). OPEN-И-12 — контакты эскалации (⏸ P1).

  • 🔁 Обновление под Yandex Cloud (DO-1): все диагностические команды в разделах ниже изначально были написаны абстрактно (pg_isready, redis-cli). После закрытия DO-1 в v8.4 будут уточнены под yc managed-postgresql cluster get, yc managed-redis cluster get и аналоги. Сейчас абстрактные команды — корректны, но в spirit Yandex CLI.

Назначение документа: закрыть пробел 🟡 №11 из конспекта анализа v8.0 — «нет runbook эксплуатации, что делать дежурному при инцидентах». Документ — практическое руководство для on-call дежурного: типовые инциденты, диагностика, шаги восстановления, эскалация. Учитывает все архитектурные решения v8.1 (RLS, реселлерская модель, chargeback, impersonation, новые cron'ы CTO-1/CTO-3).

Целевая аудитория:

  • Дежурный разработчик (роль dev_oncall) — основной читатель;
  • Админ SaaS роли super_admin / finance / compliance — для процедур, требующих действий через админку;
  • Тех. директор — для процедур эскалации и пост-инцидентного разбора.

Принципы документа:

  1. Каждая процедура — 5 секций: симптом / алерт / диагностика / действия / эскалация. Лишнего нет.
  2. Никаких архитектурных объяснений — для них есть главный файл v8.1. Здесь — только что нажать.
  3. Команды и SQL — копируемые без правок. Все параметры в <угловых скобках> подставляются дежурным.
  4. Уровни критичности соответствуют разделу 25.5 v8.1: CRITICAL (15 мин), WARNING (2 часа), INFO (best effort).

Базовые ссылки на v8.1:

  • 23.5 — список cron-задач (всех 11)
  • 23.6 — supervisor / воркеры очередей
  • 23.7 — бэкапы PostgreSQL (WAL-G)
  • 25 — мониторинг (Prometheus + Grafana, Sentry, healthcheck)
  • 25.5 — каналы алертов и SLA
  • 25.6 — on-call rotation
  • 22.10 — безопасность платежей (HMAC, idempotence)
  • 20.5 — жизненный цикл pending транзакций (CTO-3)
  • 20.6.1 — chargeback workflow (Ю-3)
  • Приложение В — state machines (для понимания валидных переходов)
  • Приложение Г — админка SaaS (роли, экраны)

Статус: v0.1 от 04.05.2026, готов к использованию с момента запуска staging. Будет дополняться по мере накопления опыта эксплуатации (раздел И.13 — журнал прецедентов).


0. Содержание

  1. Подготовка дежурного (что должно быть под рукой)
  2. Общий алгоритм при срабатывании любого алерта
  3. Webhook от crm.bp-gr.ru: пропал поток лидов
  4. Очереди: backlog растёт, воркеры не справляются
  5. Платежи: late_webhook_ignored / зависшие pending / шлюз не отвечает
  6. Tariffs:apply-scheduled не отработал в 00:00 МСК
  7. RLS-политика: подозрение на утечку между тенантами
  8. Chargeback unrecovered: алерт finance
  9. БД: сбой репликации, восстановление, full disk
  10. Бэкап тест-восстановления упал
  11. Истечение TLS-сертификата / DNS-инцидент
  12. Эскалация: куда, кому, как
  13. Журнал прецедентов (для пополнения)

1. Подготовка дежурного

При получении смены дежурства убедиться, что есть доступ ко всему ниже. Если чего-то нет — до начала смены запросить у тех. директора, не во время инцидента.

Ресурс Адрес Доступ
Sentry https://sentry.<домен> или SaaS Логин по SSO
Grafana https://grafana.<домен> Логин по SSO + роль editor
Alertmanager https://alertmanager.<домен> Чтение + silence
Bastion / SSH bastion.<домен> SSH-ключ дежурного
Production DB (read-only) postgres-ro.<домен> Через bastion + ключ + пароль роли crm_oncall_readonly
Production DB (write, через миграции) postgres-master.<домен> Только через PR + четыре глаза, не напрямую
Админка SaaS https://admin.<домен> Роль dev_oncall + 2FA
Status page (управление) https://status.<домен>/admin Логин редактора
Slack #alerts, #oncall, #sre Член этих каналов
Контакты эскалации Этот Runbook, раздел 12

Минимальная проверка работоспособности дежурства (сделать в первый день смены):

# Из своей машины через bastion
ssh bastion.<домен>
ssh app-01.<домен>  # должно пройти
psql "postgresql://crm_oncall_readonly@postgres-ro/crm" -c "SELECT 1"  # должно вернуть 1
curl -s https://<домен>/health | jq .status  # должно вернуть "healthy"

Если хоть одна команда не работает — это уже инцидент дежурства, фиксировать в #oncall сразу.


2. Общий алгоритм при срабатывании любого алерта

Прежде чем кидаться чинить — 5 шагов за 60 секунд:

  1. Подтвердить получение в Slack #alerts реакцией 👀 (чтобы команда знала что взято в работу).
  2. Открыть Grafana → дашборд «SaaS overview» → проверить, есть ли смежные алерты или это одиночный.
  3. Открыть Sentry → отфильтровать по последним 30 минутам → есть ли спайк ошибок.
  4. Проверить Status page: в плановых работах ли это (если да — отбой).
  5. Решить: это инцидент CRITICAL (продакшн лежит) или WARNING (что-то деградирует, но работает)?

Если CRITICAL → сразу:

  • стартовать тред в #oncall с заголовком 🚨 INC-YYYYMMDD-HHMM <короткое описание>;
  • создать запись в incidents_log (или Notion/Confluence до момента когда таблица появится — [OPEN-И-1]);
  • эскалация по разделу 12 параллельно с действиями;
  • НЕ откладывать публичный апдейт на Status page больше чем на 10 минут — пользователи уже видят проблему.

Если WARNING → можно работать в обычном темпе, но фиксировать ход в #oncall каждые 1530 мин.

Что НЕ делать никогда:

  • Не выкатывать срочный hotfix в production без PR и хотя бы одного reviewer'а. Лучше потерять 10 минут на review, чем уронить вторую систему.
  • Не делать DELETE, UPDATE, TRUNCATE напрямую в production-БД. Это всегда через миграцию + PR + four-eyes (даже в инциденте — если очень нужно, эскалировать на тех. директора и делать совместно).
  • Не отключать RLS-политики «временно для отладки». Это создаёт окно утечки. Использовать роль crm_admin_user (BYPASSRLS) только для конкретного запроса в read-only.
  • Не пушить в master напрямую. Все инцидентные правки — через PR с тегом incident/INC-XXX.

3. Webhook от crm.bp-gr.ru: пропал поток лидов

Симптом: алерт WebhookEndpointDown (rate(webhook_received_total[10m]) == 0 в течение 30 минут). Может также проявиться как жалобы тенантов в #support: «лиды перестали приходить».

Уровень: CRITICAL — это наш единственный источник дохода (Ю-2). Каждый час простоя = реальные потерянные лиды.

3.1. Диагностика (по порядку)

# А. Проверить, что наш endpoint вообще отвечает
curl -X POST https://<домен>/webhooks/leads \
     -H "Content-Type: application/json" \
     -d '{"test": true}' \
     -w "%{http_code}\n"
# Ожидаем: 401 (unsigned) или 400 (invalid payload) — endpoint жив
# Если 502/503/504 → проблема на нашей стороне (раздел 3.2 А)
# Если 200 на тестовый payload → у нас проблема с валидацией (раздел 3.2 Б)
-- Б. Посмотреть последние записи webhook_log
SELECT received_at, source_ip, COUNT(*) 
FROM webhook_log 
WHERE received_at > NOW() - INTERVAL '2 hours'
GROUP BY 1, 2 
ORDER BY 1 DESC LIMIT 50;
-- Если последняя запись > 30 минут назад → либо crm.bp-gr.ru не шлёт, либо мы не получаем
# В. Проверить failed_webhook_jobs
psql ... -c "SELECT failure_reason, COUNT(*) FROM failed_webhook_jobs WHERE created_at > NOW() - INTERVAL '1 hour' GROUP BY 1;"
# Если много failures с одной причиной (например, signature_invalid) → раздел 3.2 В

3.2. Действия

А. Endpoint не отвечает (502/503/504):

  1. Проверить kubectl get pods (или supervisorctl status на VM): жив ли application-pod / php-fpm.
  2. Проверить Nginx logs: tail -100 /var/log/nginx/error.log — есть ли upstream timeouts.
  3. Если pod умер — рестарт через kubectl rollout restart deployment/app или supervisorctl restart all.
  4. Если Nginx падает — перезапустить Nginx: systemctl restart nginx.

Б. Endpoint отвечает 200, но лиды не идут:

  1. Возможно, crm.bp-gr.ru сам перестал слать. Связаться с их поддержкой по контакту из договора (закреплён в Б-1).
  2. Параллельно — проверить, нет ли у них на стороне нашего IP в чёрном списке (могло быть после серии 5xx с нашей стороны ранее).
  3. Записать в #oncall факт обращения к crm.bp-gr.ru с временем.

В. Webhook'и идут, но падают на валидации:

  1. Если failure_reason='signature_invalid' массово → возможно crm.bp-gr.ru обновили секрет HMAC. Запросить у них новый, обновить в system_settings.webhook_hmac_secret через миграцию + PR.
  2. Если failure_reason='tenant_not_found' → их payload содержит external_project_id, которого у нас нет. Возможно тенант удалил проект, а crm.bp-gr.ru ещё шлёт. Это штатная ситуация — лид отбрасывается, не алерт.
  3. Если failure_reason='balance_insufficient' массово (от одного тенанта) → этот тенант исчерпал баланс на тарифе start. Это тоже штатно.

3.3. Эскалация

  • Если в течение 30 минут не понятно, проблема у нас или у crm.bp-gr.ru — эскалировать тех. директору.
  • Если crm.bp-gr.ru не отвечает на запрос 2 часа — эскалировать на бизнес-владельца (есть ли SLA в договоре, как принудить).
  • Параллельно — обновить Status page: «Деградация приёма лидов, идёт диагностика».

3.4. После восстановления

  • Записать в #sre пост-мортем: что было, сколько лидов потеряно (по данным webhook_log диффом), что сделано чтобы не повторилось.
  • Если лиды массово потеряны — обсудить с finance компенсацию пострадавшим тенантам (ручное начисление через balance_transactions(type='manual_adjustment'), требует four-eyes если > 50 000 ₽).

4. Очереди: backlog растёт, воркеры не справляются

Симптом: алерт QueueBacklog (queue_jobs_pending > 10000). Может также проявиться через Horizon dashboard — в Grafana панель «Queue length».

Уровень: WARNING (если очередь webhooks) → быстро становится CRITICAL, так как лиды стоят.

4.1. Диагностика

# Какие очереди заполнены
redis-cli LLEN queues:webhooks
redis-cli LLEN queues:default
redis-cli LLEN queues:reports
redis-cli LLEN queues:emails

# Сколько воркеров живо
supervisorctl status | grep crm
# или в k8s:
kubectl get pods -l app=crm-worker
-- Что в failed_jobs за последний час
SELECT queue, exception, COUNT(*) 
FROM failed_jobs 
WHERE failed_at > NOW() - INTERVAL '1 hour'
GROUP BY 1, 2 
ORDER BY 3 DESC;

4.2. Действия

А. Воркеры мертвы:

  1. supervisorctl restart crm-webhook-worker:* (или k8s rollout restart).
  2. Подтвердить через Horizon, что воркеры подцепили задачи.
  3. Backlog должен начать снижаться. Если нет — раздел 4.2 В.

Б. Воркеры живы, но не успевают (нагрузочный пик):

  1. Временно увеличить numprocs в /etc/supervisor/conf.d/crm-workers.conf с 4 до 8–12 (для очереди webhooks).
  2. supervisorctl reread && supervisorctl update.
  3. Следить за Grafana: если CPU app-серверов > 80% — нужно горизонтально масштабироваться, эскалация на DevOps.

В. Воркеры подбирают, но job-ы массово падают:

  1. Посмотреть failed_jobs.exception — обычно одна причина: ошибка в коде, недоступная зависимость (Sentry/email/S3), миграция не применена, RLS-политика блокирует.
  2. Если ошибка явно от свежего деплоя — rollback: kubectl rollout undo или git revert <sha> && deploy.
  3. Если ошибка от внешнего сервиса (Sentry/SMTP timeout) — это нормально для retry; если массово — отключить временно зависимость (например, переключить SMTP на резервного провайдера).

Г. Очередь failed_webhook_jobs (особый случай):

  • Это не очередь Redis, а таблица. Туда попадают webhook'и после 3 неудачных попыток.
  • Содержимое анализируется через админку (раздел «Очереди и инциденты», см. Приложение Г).
  • Ручной retry — через UI «Перезапустить» (роль dev_oncall).
  • Массовый retry — через php artisan webhook:retry-failed --since="2 hours ago".

4.3. Эскалация

  • Backlog webhooks > 10 000 в течение 30 минут → CRITICAL, эскалация тех. директору.
  • Backlog reports или emails > 10 000 — WARNING, можно дождаться рабочего времени.

5. Платежи: late_webhook_ignored / зависшие pending / шлюз не отвечает

Симптом 1: алерт finance в админке (раздел 20.5 v8.1) — late_webhook_ignored, ручной разбор. Симптом 2: жалобы тенантов «оплатил, но баланс не пополнился». Симптом 3: алерт от Alertmanager на rate ошибок к платёжному шлюзу.

Уровень: WARNING (одиночный случай) → CRITICAL если массовый сбой шлюза.

5.1. late_webhook_ignored — ручной разбор одного случая

Контекст (CTO-3): транзакция failed, поздно прилетел webhook payment.succeeded. Переход failed → success запрещён, статус не меняется, в админке алерт.

Шаги для finance / dev_oncall:

  1. Открыть админку → раздел «Биллинг» → подвкладка «Аномалии платежей» → найти запись.
  2. Посмотреть payload позднего webhook: действительно ли деньги списались у клиента?
  3. Если да (деньги ушли клиенту):
    • Вариант 1: ручное начисление через balance_transactions(type='manual_adjustment') в админке. С four-eyes если > 50 000 ₽ (ДЕФ-4).
    • Вариант 2: возврат через шлюз — если клиент не хочет лиды, попросить finance вернуть деньги.
    • В обоих случаях — комментарий в saas_admin_audit_log с ссылкой на transaction_id и обоснованием.
  4. Если нет (deal flagged как failed правильно, payload подозрительный):
    • Связаться с поддержкой шлюза (ЮKassa / Tinkoff) с idempotence_key и transaction_id.
    • Не начислять, пока не подтверждено.

5.2. Зависшие pending старше 30 минут массово

Симптом: растёт счётчик pending транзакций в Grafana, cron payments:cancel-stale отработал, но не справляется.

SELECT 
    gateway, 
    COUNT(*) FILTER (WHERE created_at < NOW() - INTERVAL '30 minutes') AS stale_30min,
    COUNT(*) FILTER (WHERE created_at < NOW() - INTERVAL '24 hours')   AS stale_hard
FROM saas_transactions 
WHERE status = 'pending' 
GROUP BY 1;
  • Если stale_hard > 0 — это нарушение CTO-3, не должно быть. Расследовать почему cron не сработал (раздел 4 — очереди или раздел 6 — cron).
  • Если только stale_30min > 100 — возможно, шлюз медленный. Проверить статус ЮKassa: https://yookassa.ru/status или Tinkoff аналогично. При деградации шлюза — это их проблема, мы только мониторим и алертим клиентов.

5.3. Шлюз полностью не отвечает

  1. Проверить публичный статус шлюза.
  2. Если шлюз down — переключиться на резервного через system_settings.payments_default_gateway (если есть driver-альтернатива; иначе ждать).
  3. Status page: «Платежи через ЮKassa временно недоступны, остатки баланса работают штатно».
  4. Не давать обещаний клиентам по таймингу — это от шлюза.

5.4. Эскалация

  • Любая проблема с биллингом затрагивающая >5 тенантов → эскалация на роль finance SaaS-админки + тех. директор.

6. Tariffs:apply-scheduled не отработал в 00:00 МСК

Симптом: утром обнаруживается, что подписки, у которых started_at = вчерашние 00:00 МСК, всё ещё в статусе scheduled. Тенанты пишут в поддержку «я сменил тариф вчера, а изменений нет».

Уровень: WARNING — биллинг временно деградировал, но данные корректны.

6.1. Диагностика

-- Сколько scheduled-подписок зависло
SELECT COUNT(*), MIN(started_at), MAX(started_at) 
FROM tariff_subscriptions 
WHERE status = 'scheduled' 
  AND started_at <= NOW();
# Посмотреть последний запуск cron
grep "tariffs:apply-scheduled" /var/log/laravel.log | tail -20
# Или через Horizon — там видно историю schedule

6.2. Действия

  1. Если cron вообще не запускался (например, system cron упал, или supervisor с воркером):

    • Проверить crontab -l -u www-data: есть ли строка * * * * * php artisan schedule:run.
    • Перезапустить supervisor: supervisorctl restart crm-default-worker:*.
    • Запустить вручную: php artisan tariffs:apply-scheduled.
  2. Если cron запускался, но падал:

    • Sentry → искать события от App\Console\Commands\TariffsApplyScheduled.
    • Типичная причина: race condition (одна active + одна scheduled нарушены — но это ловят уникальные частичные индексы, должно быть исключение).
    • Если индекс нарушен — это критичный баг, эскалация в разработку, не fixить руками в production-БД.
  3. Принудительный прогон:

    php artisan tariffs:apply-scheduled --force --verbose
    

    Если несколько подписок не прошли валидацию — Laravel напишет какие, дальше — точечный разбор.

6.3. Эскалация

Если непонятно почему cron упал — эскалация в разработку (рабочее время) или тех. директору (нерабочее время).


7. RLS-политика: подозрение на утечку между тенантами

Симптом: клиент пишет в поддержку «я вижу чужого лида / чужие транзакции / чужие данные». Или тест assertTenantIsolation упал на CI после миграции.

Уровень: CRITICAL всегда, без исключений. Даже подозрение — это инцидент. Это удар по 152-ФЗ и репутации.

7.1. Действия (НЕМЕДЛЕННО)

  1. Подтвердить получение жалобы клиента, попросить скриншот (если ПДн на нём — попросить замазать). Не спорить, не объяснять.
  2. Зафиксировать в #oncall как 🚨 INC-YYYYMMDD-HHMM Suspected RLS leak.
  3. Эскалация СРАЗУ — тех. директор + compliance + super_admin. Не ждать диагностики.
  4. Status page: НЕ обновлять до понимания причины (преждевременная публикация = паника).

7.2. Диагностика (только тех. директор + compliance)

-- Проверить, какая роль БД использовалась в подозрительном запросе
-- Лог приложения должен содержать запись с user_id и tenant_id
-- Найти соответствующий запрос в pg_stat_activity или в логе query_log

-- Проверить, что RLS включён на всех 30 таблицах
SELECT schemaname, tablename, rowsecurity 
FROM pg_tables 
WHERE schemaname = 'public' 
  AND tablename IN (SELECT tablename FROM <список 30 таблиц>);
-- rowsecurity должно быть true для всех

-- Проверить, что политики не были случайно дропнуты
SELECT schemaname, tablename, policyname 
FROM pg_policies 
ORDER BY 1, 2;
-- Должно быть 29 политик

7.3. Сценарии

Сценарий А: миграция выключила RLS на таблице. Например, ALTER TABLE ... DISABLE ROW LEVEL SECURITY; где-то проскочил. Линтер моделей на CI должен был отловить — если не отловил, это второй баг.

  • Немедленно: ALTER TABLE ... ENABLE ROW LEVEL SECURITY; через миграцию (ускоренный путь — тех. директор пушит в master, миграция деплоится, факт логируется).
  • Дальше: full audit таблицы (диффом по pd_processing_log за период когда RLS был выключен) — могла ли быть утечка кому-то ещё.

Сценарий Б: crm_app_user каким-то образом получил BYPASSRLS.

  • Срочно: ALTER ROLE crm_app_user NOBYPASSRLS;. Расследование — кто и когда дал привилегию.

Сценарий В: код приложения забыл SET LOCAL app.current_tenant_id.

  • Тогда RLS политика сработала, но current_setting('app.current_tenant_id') вернул '' или NULL — поведение зависит от политики.
  • Проверить: какие запросы шли БЕЗ middleware tenant-scoped (например, обработчик webhook до резолва тенанта, или job без TenantAwareJob-родителя).

Сценарий Г: Данные технически правильные, но клиент видит то что НЕ его (визуальный баг во фронте).

  • Это не RLS — это код фронтенда показывает кэш чужого пользователя.
  • Фикс на фронте, но аудит на бэке всё равно нужен.

7.4. После

  • Уведомить ВСЕХ тенантов которые могли пострадать (по pd_processing_log).
  • Уведомление в Роскомнадзор об инциденте — ст. 21 ч. 3.1 ФЗ-152, в течение 24 часов о факте, в течение 72 часов — о результатах внутреннего расследования. Шаблон уведомления — задача юриста (подача через compliance-роль).
  • Пост-мортем в #sre.

📝 Этот тип инцидента требует обязательной фиксации в incidents_log ([OPEN-И-1] если таблицы ещё нет) с пометкой pd_breach=true. Эта пометка триггерит специальные процедуры compliance.


8. Chargeback unrecovered: алерт finance

Симптом: алерт finance в админке: «Chargeback по тенанту X, сумма Y ₽, не покрыт балансом на Z ₽, тенант приостановлен» (раздел 20.6.1 v8.1).

Уровень: WARNING — финансовый, не технический. Тенант уже автоматически приостановлен системой.

8.1. Действия (роль finance)

  1. Открыть админку → «Биллинг» → «Chargeback» → найти запись.
  2. Посмотреть детали:
    • Сумма chargeback;
    • Дата исходного платежа;
    • Какой это тенант — давний клиент / новый / подозрительный?
  3. Решение принимает finance, но варианты:

Вариант А — реальный спор клиента:

  • Связаться с клиентом, выяснить причину.
  • Если конструктивно — попросить отозвать chargeback (через банк). После отзыва — chargeback_unrecovered_rub обнулить через manual_adjustment, статус восстановить.
  • Если клиент не идёт на контакт — оставить тенант в suspended, ждать.

Вариант Б — мошенничество / техническая ошибка:

  • Списать долг как убыток через balance_transactions(type='manual_adjustment') с обнулением chargeback_unrecovered_rub.
  • Four-eyes обязателен если сумма > 50 000 ₽ (ДЕФ-4).
  • Запись в saas_admin_audit_log с обоснованием.

Вариант В — клиент готов погасить:

  • Клиент через /billing оплачивает (тип транзакции chargeback_repayment).
  • После успеха — автоматически: chargeback_unrecovered_rub=0, статус active (если не было других причин suspended).

8.2. Эскалация

  • Сумма > 100 000 ₽ — эскалация бизнес-владельцу (нужно решение «списываем или принципиально взыскиваем»).
  • Систематические chargeback от одного шлюза → проверить настройки HMAC и idempotence keys, возможно технический баг с нашей стороны.

9. БД: сбой репликации, восстановление, full disk

9.1. Реплика отстаёт > 30 секунд

Алерт: PostgresReplicationLag (lag_seconds > 30).

-- На master:
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn,
       (extract(epoch from (now() - reply_time)))::int AS lag_sec
FROM pg_stat_replication;
  • Если lag растёт — реплика медленнее master. Возможные причины: тяжёлый запрос на реплике (SELECT ... FROM huge_table от аналитики), сеть, диск.
  • Действия: убить тяжёлый запрос на реплике (pg_terminate_backend(pid)), проверить i/o (iostat).
  • Если сеть — эскалация DevOps.

9.2. Master упал

Алерт: DatabaseDown (up{job="postgres"} == 0).

CRITICAL. Действия:

  1. Подтвердить, что master действительно недоступен (попробовать pg_isready через bastion).
  2. Эскалация ТД + DevOps немедленно.
  3. Не делать failover самому — это решение DevOps + ТД совместно. Failover требует:
    • убедиться что master действительно мёртв (а не split-brain);
    • выбрать реплику с минимальным lag;
    • переключить master/standby роли;
    • переключить connection string в DATABASE_URL (через secret-manager) — приложение само переподключится;
    • убедиться что старый master не вернулся как «второй master» (split-brain — проверить etcd/Patroni если используется).
  4. Status page: «БД недоступна, идёт восстановление».

9.3. Full disk на master

Алерт: DiskSpaceLow (disk_free / disk_total < 0.1).

  1. Что занимает место — обычно WAL-сегменты или партиции старых таблиц.

  2. Срочно: убедиться что WAL-G успешно архивирует — иначе WAL накапливается.

    ls -la /var/lib/postgresql/16/main/pg_wal | wc -l
    # Если > 100 — что-то сломалось в архивировании
    
  3. Если архивирование работает — старые WAL должны самоудаляться. Проверить настройку archive_command.

  4. Если действительно полно pg_wal — это уже близко к падению, эскалация DevOps сейчас.

  5. Никогда не удалять WAL вручную — это убьёт PITR.

9.4. Эскалация

Любые проблемы с master БД — CRITICAL, ТД + DevOps сразу.


10. Бэкап тест-восстановления упал

Симптом: алерт « КРИТИЧНО: бэкап сломан» в Slack #alerts от cron backup:test-restore (раздел 23.7.2 v8.1).

Уровень: WARNING (один прогон) → CRITICAL (два подряд).

10.1. Диагностика

  1. Открыть лог последнего прогона (где сохраняется — DevOps определяет в инфра-конфиге).
  2. Smoke-test упал на каком этапе?
    • Восстановление WAL — проверить, не corrupted ли последние сегменты в S3.
    • Старт PostgreSQL на тестовом — обычно из-за несовместимости версий.
    • Тестовый запрос — неожиданно, скорее всего реальная битая БД.

10.2. Действия

  1. Запустить вручную: php artisan backup:test-restore --verbose.

  2. Если повторно падает — эскалация DevOps.

  3. Параллельно — сделать дополнительный полный снимок прямо сейчас (через WAL-G) на случай, если текущая ситуация нестабильна:

    wal-g backup-push /var/lib/postgresql/16/main
    
  4. Не считать инцидент закрытым, пока следующий регулярный test-restore не пройдёт .

10.3. Эскалация

  • 2 подряд упавших test-restore → CRITICAL, ТД + DevOps. Это означает что мы фактически без бэкапа.

11. Истечение TLS-сертификата / DNS-инцидент

11.1. Сертификат скоро истечёт

Алерт: TLSCertExpiry (certmanager.io метрика, обычно за 14 / 7 / 1 день).

  • В Kubernetes — cert-manager обычно сам ротирует Let's Encrypt. Если не ротировал — посмотреть kubectl describe certificate ....
  • На VM — проверить cron certbot renew.
  • Ручное обновление: certbot renew --force-renewal -d <домен>, потом systemctl reload nginx.

11.2. Сертификат уже истёк

CRITICAL. Браузеры показывают warning, клиенты не могут зайти.

  1. Срочно — выпустить новый: certbot --nginx -d <домен>.
  2. Если ACME challenge не работает (DNS, провайдер) — временно подложить самоподписанный + Status page предупреждение, эскалация DevOps.

11.3. DNS-инцидент

  • Проверить, кто провайдер DNS (CloudFlare / route53 / DNS Made Easy).
  • dig +short <домен> — что отдаёт?
  • Если NS не отвечает — это у регистратора домена. Эскалация бизнес-владельцу (может потребоваться доступ к панели регистратора).

12. Эскалация

Кто Когда Канал Время доступа
Дежурный → Тех. директор Любой CRITICAL > 15 минут без понимания причины. Любой подозреваемый RLS leak. Любой инцидент с БД-master. Telegram + звонок 24/7 (по on-call rotation тех. директора)
Тех. директор → Бизнес-владелец Простой > 1 часа. Финансовый ущерб > 100 000 ₽. Решения о публичных коммуникациях / Status page при crisis. Telegram + звонок 24/7
Любой → DevOps Инфраструктурные проблемы (БД, K8s, сеть, сертификаты, DNS) Slack #sre + Telegram По on-call rotation DevOps
Любой → finance SaaS-админ Платёжные инциденты, chargeback массово Slack #finance-ops Рабочее время; вне — best effort
Любой → compliance SaaS-админ Подозрение на утечку ПДн. Обращение от Роскомнадзора. Массовые pd_subject_requests. Slack #compliance Рабочее время; вне — через тех. директора
Тех. директор → Юрист Подтверждённый pd_breach. Уведомление в Роскомнадзор за 24/72 часа. Email + звонок По договорённости с юристом

Контакты — отдельный приватный документ (Контакты_эскалации.md, доступен только on-call). Хранится в защищённом месте, не в публичном репозитории.

[OPEN-И-12] — где именно хранить контакты эскалации (1Password / Vault / Notion private). Решение DevOps + ТД.

⚠ Историческая справка. В v8.1 этот вопрос имел ID OPEN-И-2. В v8.2 ID OPEN-И-2 переиспользован для решения «Стратегия CRM-интеграций» ( закрыт 04.05.2026). Переименован в OPEN-И-12 05.05.2026 (v8.3.1) для разрешения коллизии. ID OPEN-И-11 уже занят миграциями crm_connections/crm_field_mappings ( закрыт в v8.2 в составе OPEN-И-2).


13. Журнал прецедентов

Этот раздел пополняется после каждого инцидента (post-mortem). Назначение — чтобы дежурный мог быстро найти «у нас уже было похожее, действовали так-то».

Дата INC-ID Краткое описание Уровень Решение Что улучшено в системе
(пусто на момент v0.1)

Пример заполнения после первого реального инцидента: | 15.06.2026 | INC-20260615-0342 | crm.bp-gr.ru сменили HMAC-секрет без уведомления | CRITICAL, 2 часа | Получили новый секрет, обновили system_settings, восстановили приём webhook | Добавлен алерт HighSignatureFailureRate, договорились с crm.bp-gr.ru о предуведомлении за 7 дней |


ЧАСТЬ В. РАБОЧИЕ МАТЕРИАЛЫ

В.1. Открытые вопросы для финализации

ID Вопрос Кому Приоритет Влияние
OPEN-И-1 Создать таблицу incidents_log для системного учёта инцидентов CTO P1 Закрыт 04.05.2026 в составе schema.sql v8.2 (см. шапку этого файла + Прил_М_Analiz_originala_v8_3.md §3.3, OPEN-Д-5/И-1)
OPEN-И-12 Где хранить контакты эскалации (1Password / Vault / Notion private) DevOps + ТД P1 Без этого раздел 12 не работоспособен на практике. Дефолт (05.05): временно в приватном Notion / 1Password DevOps до закрытия Б-1 + DO-4. Был OPEN-И-2 в v8.1 — переименован 05.05 (v8.3.1), т.к. ID OPEN-И-2 занят CRM-интеграциями, ID OPEN-И-11 — миграциями crm_connections.
OPEN-И-3 Периодичность перевыпуска Runbook'а: пересматривать раз в квартал? ТД P2 Без регламента документ застаревает
OPEN-И-4 Учения on-call: периодические game days с симуляцией инцидентов? ТД + DevOps P2 Сильно повышает готовность; но требует ресурсов
OPEN-И-5 Шаблон уведомления Роскомнадзора об инциденте с ПДн (24/72 часа по ст. 21 ч. 3.1 ФЗ-152) юрист P1 Без шаблона при инциденте теряется время
OPEN-И-6 Резервный платёжный шлюз — какой? Когда подключаем? DevOps + бизнес P2 Сейчас единая точка отказа; раздел 5.3 предполагает его существование
OPEN-И-7 Procedure для массового compensating credit (когда нужно начислить N тенантам по факту простоя) finance + CTO P2 Сейчас только ручное; требует кнопку в админке
OPEN-И-8 Регламент работы с Status page: кто публикует, через сколько после начала инцидента, как формулирует ТД + бизнес P1 В разделе 2 сказано «не позже 10 минут», но нет шаблонов и ответственного
OPEN-И-9 Алерт HighSignatureFailureRate (раздел 13 example) — добавить в Alertmanager DevOps P2 Конкретное улучшение, ловит сценарий 3.2 В быстрее
OPEN-И-10 Регламент пост-мортема: формат, сроки, кто пишет, кто ревью ТД P1 Без регламента обучение из инцидентов не происходит

В.2. Что обновить в смежных документах

Документ Что меняется
CRM_bp-gr_Инструкция_v8_1.md, раздел 25.6 Заменить упоминание «Runbook (документ с типовыми инцидентами и порядком действий)» на ссылку на это Приложение И
CRM_bp-gr_Инструкция_v8_1.md, раздел 28 Добавить шифр И = Runbook_ekspluatatsii_v8_1.md. Финальный список приложений: А, Б, В, Г, Д, Е, Ж, З, И
Открытые_вопросы_v8_1.md Закрыть 🟡 №11 (runbook эксплуатации); добавить OPEN-И-1..10 в раздел DevOps / эксплуатация
Админка_SaaS_v8_1_драфт.md, §4 (роли) Уточнить полномочия роли dev_oncall со ссылкой на этот Runbook (что делает в каких сценариях)
schema.sql v8.2 Добавить таблицу incidents_log (после OPEN-И-1)

В.3. Версионирование

Версия Дата Изменения
v0.1 04.05.2026 Первый Runbook в рамках сессии 03–04.05.2026, на основе архитектуры v8.1. Покрывает 9 типовых инцидентов
v0.2 (план) После закрытия OPEN-И-1, И-2, И-5 Структурные таблицы, шаблоны, контакты. Ввод в реальную работу с момента запуска staging
v0.3 07.05.2026 Дополнения по реализации 27 решений аудита C (v1.12). Новые процедуры — см. Часть Г ниже
v1.0 (план) После 6 месяцев эксплуатации Журнал прецедентов с реальными инцидентами; пересмотр процедур по итогам

Часть Г. v8.5 — процедуры по аудиту C (07.05.2026)

Источник: реестр Открытые_вопросы_v8_3.md v1.12 §13.10. Реализация — schema.sql v8.5 (коммит 038a884) + narrative v8.5 §22.13. Все процедуры ниже становятся частью обязательного operational baseline с момента деплоя v8.5 в staging.

Г.1. CTO-13: RLS smoke-test через PgBouncer (обязательно в спринте 1)

Когда: перед первым PR, который использует TenantAwareJob или SET LOCAL app.current_tenant_id. Без прохождения теста — триггер фазы 1 не открывается.

Что проверить:

-- Кейс 1: auto-commit базовый
SET LOCAL app.current_tenant_id = 1;
SELECT COUNT(*) FROM deals WHERE tenant_id = 1; -- ожидание: успех
SELECT COUNT(*) FROM deals WHERE tenant_id = 2; -- ожидание: 0 (RLS блокирует)

-- Кейс 2: reuse соединения через PgBouncer (transaction-pooling)
-- В session 1: SET LOCAL → SELECT → COMMIT.
-- В session 2 (то же физ. соединение): SELECT без SET LOCAL.
-- Ожидание: либо exception (current_setting не установлен),
--           либо value сброшен (NULL/empty).
-- Молчаливое наследование значения от session 1 → BLOCKER.

-- Кейс 3: job retry
-- TenantAwareJob с tenant_id=1 падает с exception → retry.
-- При retry: SET LOCAL = 1 в начале handle().
-- Ожидание: tenant_id корректно установлен из payload, не из памяти worker'а.

-- Кейс 4: WITH CHECK защита (OPEN-И-14)
SET LOCAL app.current_tenant_id = 1;
INSERT INTO deal_tag_pivot (deal_id, tag_id) VALUES (1, <tag_id чужого тенанта>);
-- Ожидание: RLS exception на WITH CHECK.

-- Кейс 5: REVOKE на saas-таблицах (OPEN-И-14)
SET ROLE crm_app_user;
SELECT * FROM saas_admin_users LIMIT 1;
-- Ожидание: permission denied.

Результат: все 5 кейсов либо корректно изолируют tenant_id, либо явно падают с exception. Формальный отчёт — docs/sprint1_rls_smoke.md (создаётся в спринте 1) с актуальными PG/PgBouncer версиями.

Г.2. OPEN-И-15: Cron audit:verify-chain (раз в сутки)

Расписание: 04:00 МСК ежедневно. Низкий бизнес-трафик.

Логика (псевдокод):

// app/Console/Commands/AuditVerifyChain.php
foreach (['auth_log','activity_log','pd_processing_log',
          'saas_admin_audit_log','balance_transactions'] as $table) {
    $lastChecked = Cache::get("audit_chain_last_id:$table", 0);
    $rows = DB::table($table)->where('id','>',$lastChecked)
              ->orderBy('id')->get();
    $prevHash = DB::table($table)->where('id','<=',$lastChecked)
                  ->orderByDesc('id')->value('log_hash');
    foreach ($rows as $row) {
        $expected = hash('sha256', ($prevHash ?? '') . json_encode($row), true);
        if ($expected !== $row->log_hash) {
            Sentry::captureMessage("audit_chain_break: $table id={$row->id}", 'critical');
            DB::table('incidents_log')->insert([/* type='audit_chain_break' */]);
        }
        $prevHash = $row->log_hash;
    }
    Cache::put("audit_chain_last_id:$table", $rows->last()?->id ?? $lastChecked);
}

Performance: инкрементально с last_id, полная проверка ~5 минут на 50М строк (одноразово при первой инициализации).

При обнаружении break:

  1. Sentry severity=critical → email админам SaaS + OnCall.
  2. incidents_log insert type='audit_chain_break', severity='high'.
  3. Не блокируем работу системы — audit-таблицы продолжают писаться. Расследование — отдельный процесс с привлечением CTO.

Г.3. OPEN-И-17: Cron secrets:notify-expiring (раз в сутки)

Расписание: 09:00 МСК ежедневно.

Логика: SELECT api_keys WHERE expires_at <= NOW() + INTERVAL '30 days' AND is_active=TRUE. GROUP BY (tenant_id, user_id). Один email на группу в день. Subject: «Истекают API-ключи: {N} шт. через {3-30} дней». Тело — список с key_prefix, expires_at, deep-link на «Продлить».

Действие пользователя: клик → /api-keys → у каждого ключа кнопка «Продлить на год» (expires_at = NOW() + INTERVAL '365 days'). Аудит в activity_log event=api_key.extended.

При истечении (expires_at <= NOW()): ключ автоматически переходит в is_active=FALSE через cron api_keys:deactivate-expired (раз в час). Запросы с этим ключом → 401.

Г.4. OPEN-И-21: Anti-DDoS компоненты

Г.4.1. Nginx limit_req_zone

Файл nginx/conf.d/ratelimit.conf:

limit_req_zone $binary_remote_addr zone=login:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/s;
limit_req_zone global zone=global:1m rate=1000r/s;

server {
    location /admin/login {
        limit_req zone=login burst=5 nodelay;
        limit_req zone=global;
    }
    location /api/v1/ {
        limit_req zone=api burst=20 nodelay;
        limit_req zone=global;
    }
}

Deployment: через CI/CD job deploy-nginx-config. Перед applied — nginx -t. После — graceful reload.

Г.4.2. Yandex SmartCaptcha

Configuration: config/services.php секция yandex_captchaclient_key, server_key (env, в Yandex Lockbox).

Frontend: Vue-компонент <YandexCaptcha @verified="captchaToken = $event" /> оборачивает <v-form> страниц /register, /login (после 2 неудач), /billing/topup.

Backend: middleware App\Http\Middleware\VerifyYandexCaptcha проверяет captcha_token через POST к https://captcha-api.yandex.ru/validate. При неудаче → 429.

Бюджет: ~5 000 ₽/мес при 50К challenges (стандартный pricing 2026).

Г.4.3. Disposable email blacklist

Cron accounts:refresh-disposable-list — раз в неделю (среда, 03:00 МСК):

curl -fSL https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf \
    -o /var/www/liderra/storage/app/disposable-domains.txt

Использование: Laravel validation rule App\Rules\NotDisposableEmail на RegisterController::register(). Disposable email → ошибка валидации «Используйте корпоративный email».

Г.5. OPEN-И-22: Per-tenant DEK + crypto-shred backup

Архитектура:

  • Yandex Cloud KMS хранит per-tenant DEK (Data Encryption Key) AES-256.
  • При создании tenant'аBackupService::createTenantDek($tenantId) создаёт KMS key с tag tenant_id=<id>.
  • Backup tenant'а: pg_dump → tarball → encryption envelope (random AES key + KMS-DEK) → upload в Object Storage s3://liderra-backups/tenants/{id}/{date}.tar.enc + .envelope.
  • Restore: encrypted envelope → расшифровка через KMS-DEK → расшифровка tarball → pg_restore.

Crypto-shred при удалении tenant'а (после tenants.deleted_at + 30 days):

  1. BackupService::cryptoShred($tenantId)yc kms key destroy --id <tenantDek>.
  2. Backup tarballs остаются в Object Storage, но без DEK расшифровать невозможно.
  3. Через ещё 60 дней — lifecycle policy удаляет физически (defense-in-depth).

Преимущество: 152-ФЗ ст.21 «прекращение обработки» удовлетворяется криптографическим erasure — мгновенно и атомарно.

Г.6. OPEN-И-24: pg_anonymizer для staging

Цель: безопасная репликация prod → staging без раскрытия ПДн (152-ФЗ ст.6).

Расширение pg_anonymizer ставится в фазе 3 (Прил. Н).

Процедура staging:refresh-from-prod (раз в неделю или по запросу):

# 1. Backup prod
pg_dump -Fc -h prod-pg.liderra.ru -U crm_admin_user liderra > prod-snap.dump

# 2. Restore в staging-db
pg_restore -h staging-pg.liderra.ru -U postgres -d liderra_staging prod-snap.dump

# 3. Apply masking
psql -h staging-pg.liderra.ru -d liderra_staging <<SQL
SELECT anon.start_dynamic_masking();
SECURITY LABEL FOR anon ON COLUMN users.email IS 'MASKED WITH FUNCTION anon.fake_email()';
SECURITY LABEL FOR anon ON COLUMN users.phone IS 'MASKED WITH FUNCTION anon.fake_phone()';
SECURITY LABEL FOR anon ON COLUMN users.totp_secret IS 'MASKED WITH VALUE NULL';
SECURITY LABEL FOR anon ON COLUMN deals.phone IS 'MASKED WITH FUNCTION anon.fake_phone()';
SECURITY LABEL FOR anon ON COLUMN deals.contact_name IS 'MASKED WITH FUNCTION anon.fake_first_name()';
SECURITY LABEL FOR anon ON COLUMN deals.comment IS 'MASKED WITH VALUE NULL';
-- ... аналогично для api_keys, saas_admin_users.
SELECT anon.anonymize_database();
SQL

CI: GitHub Actions job staging-refresh — раз в неделю по cron, либо gh workflow run staging-refresh.yml ручной trigger.

Запрет: прямые pg_dump prod → restore staging без anonymization — pre-commit hook + отказ деплоя при detection raw-prod-dump в staging.

Г.7. OPEN-И-13: Yandex 360 SSO setup

Шаги (для DevOps в фазе 1, спринт 11+):

  1. В Yandex 360 Admin Panel создать «приложение» типа OIDC. Получить client_id + client_secret.
  2. Положить в Yandex Lockbox: secret/yandex-sso/client-id, secret/yandex-sso/client-secret.
  3. config/services.php — секция yandex360 (client_id, client_secret, redirect_uri, authorize_url, token_url, userinfo_url).
  4. Routes: Route::get('/admin/auth/yandex/callback', [SaasAdminAuthController::class, 'handleSsoCallback']);.
  5. Создать первый break-glass: super_admin с sso_provider='local', is_break_glass=TRUE. Длинный random-пароль в 1Password DevOps vault. TOTP enrolled.
  6. Тестовый OIDC-вход → проверка JIT-create + login.
  7. incidents_log запись event='yandex_sso_enabled'.

Откат: при неработоспособности Yandex 360 — break-glass-аккаунт + временное переключение is_break_glass=TRUE для всех super_admin через прямой SQL под crm_admin_user. После восстановления IDP — обратно FALSE.

Г.8. OPEN-И-25: Cron leads:escalate-stale

Расписание: каждые 30 мин (*/30 * * * *).

Логика:

SELECT id, tenant_id, project_id, manager_id, escalated_count
FROM deals
WHERE assigned_at IS NOT NULL
  AND assigned_at + INTERVAL '4 hours' < NOW()
  AND status NOT IN ('closed','rejected','spam')
  AND escalated_count < 3
ORDER BY assigned_at ASC
LIMIT 100;

Для каждого:

  1. escalated_count++.
  2. Reassign: если assignment_strategy='manual' — supervisor проекта; если round_robin/least_loaded — следующий active member из project_user_assignments.
  3. Email (см. §17.9.2 narrative) — старому + новому менеджеру + админу тенанта при escalated_count >= 2.
  4. activity_log event=deal.escalated.

Индекс: (tenant_id, assigned_at) WHERE status NOT IN ('closed','rejected') — для эффективного scan.

Г.9. Биз-24: Cron payments:notify-stale

Расписание: раз в час (0 * * * *).

Логика: SELECT saas_invoices WHERE status='waiting_payment' AND created_at + INTERVAL '48 hours' < NOW() AND notified_finance=FALSE. Для каждого: email finance-роли + bell-нотификация в /admin/billing/stale-invoices + notified_finance = TRUE.

Дополнительно (default off): через 7 дней повторный алерт + создание incidents_log type='payment_overdue'.

Конфигурация: system_settings ключи payments_notify_stale_threshold_hours/repeat_hours/enabled.


Драфт v0.3 от 07.05.2026. Часть Г добавлена в рамках реализации v8.5 (коммит 038a884 + narrative v8.5).