Разработка

Документация для разработчиков: сервисы, базы, интеграции. Сотрудникам отделов — в книге AmoCrm.

Ядро данных

Реплика amoCRM и шлюзы, через которые сервисы ходят в CRM.

Ядро данных

Реплика amoCRM (amo_db)

Для разработчиков. Подробности устройства — в README репозитория Buharestskie/amo_db.

Что это

amo_db — копия нашего аккаунта amoCRM в обычной PostgreSQL. Сделки, контакты, задачи, примечания, неразобранное, каталоги, справочники и журнал событий. Изменение из amoCRM попадает в базу за несколько секунд.

Если сервису нужно читать данные amoCRM, он читает их отсюда, а не из API. У API лимит в 7 запросов в секунду на интеграцию и 50 на весь аккаунт — его делят все наши сервисы, виджеты и телефония, и когда он кончается, amoCRM сначала отвечает ошибками, а потом блокирует аккаунт. Из базы можно выбирать что угодно и сколько угодно.

Писать в amoCRM через базу нельзя: у всех ролей только чтение. Запись — как и раньше, через API amoCRM.

Как получить доступ

  1. Попросить администратора завести роль с доступом «только чтение» к базе amo_db. Роли выдаются через панель pg_provisioner, пароль показывается один раз.
  2. Попросить внести в список доступа внешний IP сервера, с которого будет подключение. Для своей машины разработчику достаточно запустить ./scripts/dev-db-access.sh из репозитория amo_db — скрипт сам определит адрес. Если адрес часто меняется, есть туннель поверх SSH: ./scripts/db-tunnel.sh.
  3. Подключаться к db.donula26.ru, порт 6432, база amo_db.

Порт 6432 — это пулер соединений. Он возвращает соединение в общий пул после каждой транзакции, поэтому не работают LISTEN/NOTIFY, временные таблицы и сессионные SET. Если приложение на asyncpg, кэш prepared statements нужно выключить (statement_cache_size=0), иначе под нагрузкой начнут плавать ошибки prepared statement … does not exist.

Четыре правила чтения

ПравилоПочему
Фильтровать is_deleted = falseУдалённое в amoCRM здесь не стирается, а помечается. Без фильтра в выборку попадут удалённые сделки и контакты.
В справочниках брать is_current = trueСтатусы, воронки, поля, пользователи хранятся с историей версий: переименованный статус остаётся расшифровываемым в старых сделках. Для текущего названия нужна текущая версия.
Кастомные поля — из amo_custom_field_valuesТам значения развёрнуты построчно, по ним удобно фильтровать. Разбирать JSONB в amo_leads.custom_fields не нужно.
Поиск контакта по телефону — через amo_phone_tails(phones)phone_normalized — только первый телефон контакта. Поиск по любому номеру: WHERE amo_phone_tails(phones) && ARRAY['9001112233'], номер — последние 10 цифр.

Главные таблицы

ТаблицаЧто в ней
amo_leadsСделки
amo_contactsКонтакты
amo_tasksЗадачи
amo_notesПримечания: звонки, SMS, превью писем. Переписки из мессенджеров здесь нет
amo_linksСвязи между сущностями — например, какие контакты у сделки
amo_eventsЖурнал изменений аккаунта: смены этапов, ответственных, удаления
amo_statuses, amo_pipelines, amo_users, amo_custom_fieldsСправочники, которыми расшифровываются id в сделках

Все таблицы поле за полем — в MODELS.md в репозитории.

Чего в базе нет

Как понять, что данные свежие

https://replika.donula26.ru/sync/state показывает каждый поток синхронизации. Смотреть на last_success_at и last_error: если успех был в последние минуты и ошибки нет, поток работает. На lag_seconds не ориентироваться — у удалений и каталогов он часами растёт просто потому, что меняться нечему.

Если поток встал, мониторинг сам пишет в Telegram.

Ядро данных

Как ходить в amoCRM из своего сервиса

Для разработчиков. Подробности устройства — в README репозитория Buharestskie/amo_queue.

Коротко

Если ваш сервис читает данные amoCRM — читайте их из реплики, см. «Реплика amoCRM (amo_db)». Если сервис пишет в amoCRM или ему нужно то, чего в реплике нет, — ходите через шлюз https://amo-queue.donula26.ru, а не напрямую в amoCRM.

Зачем шлюз

amoCRM пускает не больше 7 запросов в секунду от одной интеграции и не больше 50 от всего аккаунта. Превышение — ошибка 429, повторные превышения — блокировка всего аккаунта: встают все виджеты, дашборды и телефония разом.

Каждый сервис по отдельности может аккуратно держать свои 7 запросов, но про соседей он не знает — и вместе они пробивают общие 50. Шлюз ставит запросы всех сервисов в одну очередь и следит за обоими лимитами сразу.

Как подключиться

Поменять в конфиге сервиса один адрес — больше ничего:

БылоСтало
https://donula.amocrm.ru/api/v4/leads?with=contactshttps://amo-queue.donula26.ru/api/v4/leads?with=contacts

Пути, методы, тело запроса и заголовок Authorization: Bearer <токен> остаются прежними. Регистрировать сервис в шлюзе не нужно: он отличает интеграции по токену.

Обязательно передавайте имя сервиса в заголовке X-Integration-Name:

curl https://amo-queue.donula26.ru/api/v4/account \
  -H "Authorization: Bearer $AMO_TOKEN" \
  -H "X-Integration-Name: my-service"

Без имени в статистике и алертах виден только кусок хэша токена, и понять, чей это сервис, уже нельзя. Имя — латиница, цифры, точка, дефис, подчёркивание, до 40 символов.

Что может ответить шлюз

Почти всегда вы получаете ответ самой amoCRM без изменений. Свои ответы шлюз отдаёт в четырёх случаях:

КодЧто значитЧто делать
401Нет заголовка AuthorizationПередать токен
429 с телом gateway queue is fullВаш сервис прислал больше, чем его очередь разгребает за 10 секундПовторить через Retry-After и растянуть отправку во времени. Это не amoCRM и не сбой шлюза — это ваш сервис шлёт пачкой
502amoCRM недоступнаПовторить позже
504amoCRM не ответила за 30 секундПовторить позже; если повторяется — упростить запрос

Сколько можно слать

На один токен — 6 запросов в секунду, на все сервисы вместе через шлюз — 30. Оставшиеся до 50 — запас для тех, кто в amoCRM ходит напрямую: реплика, телефония, сторонние виджеты.

Запросы сверх 6 в секунду не отбрасываются сразу, а ждут своей очереди до 10 секунд. Поэтому короткие всплески проходят незаметно, а массовые выгрузки — нет: их надо отправлять равномерно или разбивать на части с паузами.

Если фоновая выгрузка мешает быстрым запросам того же сервиса (кнопки в виджете начинают тормозить), заведите для фоновой задачи отдельную интеграцию в amoCRM со своим токеном — у неё будет своя очередь.

Лендинги ходят не сюда

Бэкенды квиз-лендингов (mfctest, bfl_fspp, do-nula-refinancing-2 и подобные) отправляют заявки не через шлюз, а через отдельный сервис amocrm-service — https://donula26.ru/service/amocrm/. Причины:

Пускают туда только известные адреса. Новый лендинг — это новая строка в списке доступа сервиса: попросите администратора добавить его до запуска, иначе на каждую заявку будет ответ 403 Caller IP is not permitted. Всё остальное — ручки, коды ответов, что делать при сбое — в README репозитория Buharestskie/amocrm-service.

Как посмотреть, что с вашим сервисом

Если у вашего сервиса растёт queue_full в счётчиках — он шлёт пачками, и часть запросов до amoCRM не доходит.

Ядро данных

Вход в виджеты amoCRM

Для разработчиков виджетов. Подробности устройства — в README репозитория Buharestskie/widget_auth.

Зачем отдельный сервис

Виджет живёт внутри интерфейса amoCRM и своему бэкенду может предъявить только одно — токен X-Auth-Token, который amoCRM подписывает секретным ключом интеграции. Если проверять этот токен в каждом бэкенде, секрет интеграции окажется во всех виджетах сразу, и утечка из любого из них откроет все.

Поэтому секрет хранится в одном месте — в сервисе widget_auth на https://widget-auth.donula26.ru. Он проверяет токен amoCRM и выдаёт взамен свой, короткоживущий, подписанный отдельным секретом конкретного бэкенда. Бэкенд виджета секрета интеграции не видит никогда.

Сейчас так входят «Файлы сделки на Яндекс.Диске», чек-листы и шаблоны сообщений.

Как это выглядит со стороны виджета

  1. Виджет вызывает обмен через $authorizedAjax — amoCRM сама подставит X-Auth-Token:
    self.$authorizedAjax({
      url: 'https://widget-auth.donula26.ru/api/auth/exchange',
      method: 'POST'
    }).done(function (response) {
      // response.access_token, response.expires_in (600 секунд)
    });
  2. Полученный access_token виджет передаёт своему бэкенду в заголовке Authorization: Bearer <access_token>.
  3. Через 10 минут токен истекает — бэкенд ответит 401, и виджет должен заново получить токен.

Как бэкенд проверяет токен

Токен — JWT, алгоритм HS256. Проверять обязательно все четыре вещи:

ЧтоЧем
ПодписьACCESS_TOKEN_SECRET — свой у каждого бэкенда
Аудитория audACCESS_TOKEN_AUDIENCE — своя у каждого бэкенда, например yandex-widget-backend
Издатель issACCESS_TOKEN_ISSUER=amocrm-widget-auth-service — общий
Срок exp, nbfБиблиотека JWT проверяет сама; допуск на расхождение часов — несколько секунд
payload = jwt.decode(token, ACCESS_TOKEN_SECRET, algorithms=["HS256"],
                     audience=ACCESS_TOKEN_AUDIENCE, issuer=ACCESS_TOKEN_ISSUER, leeway=5)

Кто пришёл — в полях account_id и user_id (то же в sub как "account_id:user_id"). Это id пользователя amoCRM — по нему и решайте, что ему можно.

Не пропускайте проверку aud: именно она не даёт токену одного виджета пройти в бэкенд другого.

Подключить новый виджет

Делает администратор, у которого есть доступ к серверу:

  1. В настройках интеграции amoCRM взять её ID (client_uuid) и секретный ключ.
  2. Добавить виджет в список widget_auth: ID и секрет интеграции, новый случайный секрет бэкенда (от 32 символов) и уникальную аудиторию для него. Перезапустить сервис — список читается только при старте.
  3. В настройки бэкенда виджета вписать тот же секрет бэкенда, ту же аудиторию и общий издатель.

Порядок команд и формат записи — в README репозитория, раздел «Подключить новый виджет».

Если вход не работает

ОтветЧто проверить
401 Missing X-Auth-Token от widget_authЗапрос идёт не через $authorizedAjax, а обычным $.ajax или fetch — amoCRM подставляет токен только в первом случае
401 Invalid X-Auth-Token от widget_authВиджета нет в списке, неверный секрет интеграции или не совпала аудитория входящего токена. Обычно ошибка в только что добавленной записи
401 от своего бэкендаТокен истёк (прошло больше 10 минут) или секрет / аудитория в бэкенде не совпадают с записью в widget_auth
Ошибка CORS в консоли браузераВиджет открыт не в нашем аккаунте amoCRM — обмен разрешён только со страниц donula.amocrm.ru

Оценка звонков

Оценка звонков

Как устроена оценка звонков

Для разработчиков. Состояние на 24.09.2026. Подробности — в README репозиториев Buharestskie/amocrm_transcribe_service и Buharestskie/yandex_transcribe_service.

Коротко

Все дашборды — МПП2, МОС и МПП1 — расшифровывают и оценивают звонки через один сервис: amocrm_transcribe_service (в конфигах дашбордов он называется «S2T apiscrm»). Он отдаёт запись в распознавание речи A5X, раскладывает реплики на менеджера и клиента и оценивает разговор языковой моделью A5X по промпту, который прислал дашборд. Критерии оценки живут не в сервисе, а в «Настройках» каждого дашборда.

Путь звонка

  1. Звонок попадает в дашборд из телефонии — Sipuni, МегаФон или своя PBX: вебхуком или синхронизацией истории.
  2. Дашборд решает, что оценивать, по своим правилам: МПП2 — серия звонков после входа сделки в «Принимают решение», МОС — звонки по касаниям, рискам и рекомендациям, МПП1 — звонки охотников. Звонки одного клиента за день склеиваются в серию.
  3. Запрос в сервис — POST /internal/transcribe-analyze на внутренний порт сервиса на global (адрес — в README): ссылки на записи, тип телефонии у каждой, промпт и адрес для ответа. Сервис сразу отвечает 202.
  4. Распознавание — запись целиком уходит в s2t.a5x.pro. Левый канал становится SPEAKER_00, правый — SPEAKER_01.
  5. Роли — кто из них менеджер, сервис решает по типу телефонии, не по тексту (таблица ниже).
  6. Оценка — сегменты уходят в llm.a5x.pro (модель qwen36-35b-a3b): промпт дашборда — системным сообщением, разговор — в виде [секунда роль] текст.
  7. Ответ — сервис присылает дашборду обратный вызов: расшифровку, ответ модели (сырой и разобранный JSON) и расход — секунды аудио и токены. Дашборд сохраняет разбор и показывает его: МПП2 — на вкладке «Качество» и в виджете «Оценка разговоров» в карточке сделки, МОС — в касаниях, рисках и рекомендациях, МПП1 — в карточке звонка.
ДашбордКуда приходит ответГде промпты
МПП2/api/transcriptions/callbackНастройки: две консультации, касания, тёплый пул
МОС/api/telephony/webhooks/transcribeНастройки: касания, риски, рекомендации
МПП1/api/v1/internal/transcribe/callbackНастройки: гейт и QA-аудитор

Каналы по телефониям

ТелефонияМенеджерКлиент
Sipuniправый каналлевый
МегаФонлевыйправый
Своя PBXправыйлевый

Запись должна быть стерео: моно-запись получает ошибку, роли не угадываются. Подключаете новую телефонию — сначала строка в этой таблице (src/speaker_mapping.py), иначе менеджер и клиент перепутаются во всех разборах.

Резерв: Yandex

yandex_transcribe_service (тоже на global) — распознавание SpeechKit и оценка YandexGPT в два этапа (гейт, затем полный разбор). Раньше был основным, с 17.09.2026 рабочих запросов нет: остался кнопкой ручной перерасшифровки «через Yandex» в МПП2 и МПП1. Каталог Yandex Cloud при этом живой и платный.

Ещё два репозитория — assembly_transcribation (AssemblyAI) и gemini_service (Gemini) — нигде не развёрнуты.

Если оценки не приходят

  1. Жив ли сервис: с global — curl -s localhost:8083/health.
  2. Доходят ли запросы: docker logs --since 1h amocrm_transcribe_service-apiscrm-transcribe-1 | grep transcribe-analyze.
  3. Ошибки — в GlitchTip, проект сервиса, и в обратных вызовах с полем ошибки: их дашборд показывает у звонка.
  4. Сервис перезапускали? Задачи в работе живут в памяти и при перезапуске теряются. МОС повторяет звонок без ответа через час (потом через 30 минут и 2 часа, всего четыре попытки), МПП1 — задачей раз в 20 минут.
  5. Ответ пришёл, а разбора нет — модель ответила не JSON: смотреть сырой analysis_text в данных звонка и промпт в «Настройках».

Что стоит денег

Каждый разбор — минуты распознавания A5X и токены модели. Ключ у распознавания и модели сейчас один: отзовут — встанут оба этапа. Правка промпта в «Настройках» действует только на новые звонки; старые заново не оцениваются.

Боты и уведомления

Боты и уведомления

Приём заявок: bug_hunter

Для разработчиков. Состояние на 24.09.2026. Как пользоваться ботом сотрудникам — статья «Как сообщить об ошибке». Подробности и запуск — README Buharestskie/bug_hunter_bot.

Коротко

bug_hunter — приём заявок в разработку: сотрудник заводит ошибку или задачу в мини-приложении Telegram, заявка уходит ответственному за сервис, встаёт в общую очередь и связывается с задачей в YouGile. Разработчик работает в админке, в мини-приложении, в YouGile или через MCP-сервер из Claude Code — статус и переписка синхронизируются.

Где живёт

Серверulrica (Алматы — оттуда доступен Telegram); каталог и контейнеры — в README
Адресhttps://bug-hunter.donula26.ru (nginx на сервере)
Бот@donula_bug_hunter_bot, вебхук Telegram
Админка/admin — вход через Telegram, только ответственные; запасной вход — /start login в боте
Мини-приложение/app — всем, кроме забаненных

Один процесс FastAPI: вебхук Telegram, API, /mcp, /yougile/webhook и статика. Данные — Postgres, черновики ввода в чате — Redis, вложения — диск /data/media. Фронт собирается на машине разработчика и заливается готовым: Node на сервере нет. Миграции накатываются на старте контейнера.

Заявка: путь и статусы

СтатусСмысл
Новаяждёт разбора ответственным
На согласованиизадача ждёт согласующего; в очереди не стоит
В очередипринята, ждёт исполнителя
В работекто-то взял
Нужны детализадан вопрос автору; ответ автора возвращает прежний статус
Готовосделано; автор задачи может вернуть в работу один раз за 14 дней
Отклонена / Не баг / Дубль / Отклонена при согласовании / Отозваназакрыта без выполнения

Куда встаёт новая задача: автор с правом согласования — сразу в очередь; «незнакомый» автор (ни одной разобранной заявки) — в «Новая», решает ответственный; остальные — на согласование, если есть согласующий уровнем выше, иначе в «Новая».

Очередь: срочность (Вне очереди → Срочно → В ближайшее время → Не срочно), затем уровень автора (только у задач), затем дата. У ошибки срочности нет, пока её не назначит ответственный. Объединённые заявки занимают одно место.

Кому уходит новая заявка: ответственным за сервис; если их нет — всем ответственным с пометкой «Ответственный за сервис не назначен». Задачи на согласовании не уведомляют никого — у согласующих только счётчик в приложении.

Переписка двигает статус: не внутреннее сообщение разработчика переводит заявку в «Нужны детали» (даже из «В работе»), ответ автора возвращает её обратно. Внутренние заметки статус не трогают и автору не видны.

Объединение: присоединённые заявки получают статус и задачу YouGile главной, смена статуса и срочности идёт на всю группу, автору — одно уведомление на все его заявки группы.

Каталог сервисов

Каталог — репозитории GitHub организации Buharestskie (сверка раз в час) плюс сервисы, заведённые руками. Новый репозиторий появляется скрытым: админам приходит «В GitHub появились новые репозитории… Задайте им названия, иначе они не будут видны в боте». Пока в админке «Сервисы» ему не дали человеческое имя и не включили видимость, сотрудники его не выберут. Архивные репозитории из бота пропадают сами. Там же назначаются ответственные за сервис — от этого зависит, кому уходят заявки.

YouGile

MCP-сервер

POST https://bug-hunter.donula26.ru/mcp, JSON-RPC, заголовок Authorization: Bearer <MCP_TOKEN>. Действует от имени первичного админа с наименьшим Telegram ID из ADMIN_TELEGRAM_IDS.

ИнструментЧто делает
list_bugs, get_bug, get_queueсписок, заявка целиком с перепиской и историей, очередь с местами
set_statusстатус группы: уведомления автору, перенос в YouGile
set_urgencyсрочность группы, стикер в YouGile
route_taskзадача «незнакомца»: принять или отправить на согласование
merge_bugs, unmerge_bugобъединить дубли / отсоединить
add_noteсообщение в заявку, по умолчанию внутреннее
create_yougile_taskзавести задачу в YouGile
list_servicesкаталог сервисов

MCP ведёт себя не во всём как API: add_note с internal=false не ставит «Нужны детали» и не уходит в YouGile; set_status «В работе» для уже взятой заявки молча ничего не делает (API ответил бы 409).

На что наступают

Боты и уведомления

Релей GlitchTip в Telegram

Для разработчиков. Состояние на 24.09.2026. Подробности — README Buharestskie/gitchtip_tg_bot. Отчёт по оплатам для группы ОС описан отдельно — статья «Отчёт по оплатам группы ОС».

Коротко

gitchtip_tg_bot — релей в Telegram. Принимает HTTP-вебхуки и пересылает их в чаты: алерты GlitchTip, обращения «Сообщить о проблеме» из дашбордов и виджетов, отчёт смены менеджера МОС. Сам следит, жив ли GlitchTip, и раз в день собирает отчёт по оплатам группы ОС.

Алерт GlitchTip, обращение пользователя и монитор
Алерт GlitchTip, обращение пользователя и монитор

Где живёт

Серверulrica — с российских площадок api.telegram.org недоступен; каталог, контейнер и адрес — в README
АдресHTTP без TLS и без reverse proxy, на отдельном порту ulrica
Коддоставляется git-бандлом: у ulrica нет доступа к GitHub

Что куда уходит

Все пути защищены одним токеном в адресе: /hook/<WEBHOOK_TOKEN>. Чужой токен — 401.

Кто шлётПутьКак бот понимает, что этоЧат
GlitchTip (все проекты)/hook/…всё, что не подошло нижеTELEGRAM_CHAT_ID
«Сообщить о проблеме»: МОС, МПП1, МПП2, виджет Яндекс.Диска, return_amo_hook_service/hook/… (у отправителя — FEEDBACK_WEBHOOK_URL)в теле есть непустой aliasTELEGRAM_CHAT_ID
Отчёт смены из виджета МОС/mos-workday-report/… (у отправителя — WORKDAY_REPORT_WEBHOOK_URL)в теле есть объект reportWORKDAY_REPORT_CHAT_ID
Ручной запуск отчёта по оплатамPOST /payments-report/…—PAYMENTS_REPORT_CHAT_ID

Тип определяет тело, а не путь: отчёт смены, присланный на /hook/, всё равно уйдёт в чат отчётов.

Из пейлоада GlitchTip читаются только text и attachments[].title / title_link / text. Уровень события не читается — у любого алерта шапка «🔴 GlitchTip».

Подключить новый проект GlitchTip

  1. В GlitchTip, в настройках проекта, — правило алерта с интеграцией generic webhook.
  2. Адрес — …/hook/<WEBHOOK_TOKEN> релея: проще всего скопировать из правила любого уже подключённого проекта; адрес — в README, токен — в .env релея на сервере.
  3. Как часто алертить, решает правило в GlitchTip: сам релей ничего не фильтрует и не склеивает — один POST, одно сообщение.

Монитор GlitchTip

Раз в 5 минут релей запрашивает GLITCHTIP_HEALTH_URL (таймаут 10 секунд). Пишет только при смене состояния: «🔴 GlitchTip недоступен» с причиной и «🟢 GlitchTip снова доступен». Одного неудачного опроса хватает для алерта; падение и подъём между двумя опросами не заметит.

На что наступают

Сервисы вокруг amoCRM

Сервисы вокруг amoCRM

Дедупликация сделок: anti_double_deals

Для разработчиков. Состояние на 24.09.2026. Подробности, переменные и выкатка — README Buharestskie/anti_double_deals.

Коротко

anti_double_deals не даёт заводить в amoCRM (donula.amocrm.ru) повторные сделки на одного клиента и раскладывает новые заявки по этапам. На каждую новую сделку он сливает контакты с тем же телефоном, решает, какая сделка лишняя, убирает её в этап для дублей и переносит оставшуюся в рабочий этап. Всё, что сделал, пишет в журнал.

Как обрабатывается сделка

  1. Вебхук о новой сделке (штатный amoCRM или Sensei) → api ставит задачу в Celery на каждую сделку. worker берёт блокировку сделки в Redis.
  2. Все телефоны иностранные (казахстанские +7 7xx тоже) → этап для дублей, тег «Иностранный номер». Конец.
  3. Слияние контактов. Ищутся контакты с теми же последними 10 цифрами телефона. Выживает самый старый, остальные сливаются в него. В публичном API слияния нет — используется внутренний /ajax amoCRM, для которого worker входит в веб-интерфейс браузером Playwright под служебным пользователем; сессия хранится в Redis. После слияния все сделки группы заново привязываются к выжившему контакту.
  4. Кто дубль. Если у контакта есть более старая активная сделка в одной из 11 «рабочих» воронок (не в статусе 143) — дублем считается новая: этап для дублей, тег «Дубль новая». Иначе в этап для дублей уходят старые неактивные сделки с тегом «Дубль старая», а новая остаётся.
  5. Маршрутизация. Оставшаяся сделка переносится в этап группы A или B по полю-списку 664291 (пять значений — группа A, остальное и пустое — B).
  6. Журнал. Исход пишется в lead_routing_log, одна строка на сделку: «Маршрутизация», «Дубль новая», «Иностранный номер», «В обработке». Страница «Журнал сделок» показывает последние 100 записей со ссылками на сделки.

Страховка: beat раз в 30 минут прогоняет сделки старше 5 минут, застрявшие на входном этапе, и раз в 5 минут лечит сделки, больше 10 минут висящие «В обработке».

Откуда данные: историю (кто ещё с этим телефоном, сделки контакта, справочники) сервис читает из реплики amo_db, с откатом на API при ошибке. Всё, от чего зависит решение, и всё, что читается сразу после записи, — из amoCRM напрямую: реплика отстаёт на секунды, в худшем случае на минуты.

Где живёт

Сервер global, compose из шести контейнеров: api, worker, beat, frontend, postgres, redis. Наружу — через шлюз donula26.ru. Каталог на сервере без git: выкатка копированием, порядок — в README. GitHub Actions deploy.yml проду не соответствует — коммиты в main пока только с [skip ci]. База бэкапится ежедневно, хранение 14 дней. В учёте обращений сервис называется «Журнал маршрутизации лидов».

На что наступают

Виджеты amoCRM

Виджеты amoCRM

Встречи Контур.Толка из сделок: contur_talk_widget

Для разработчиков и тех, кто решает, нужна ли эта связка. Состояние на 25.09.2026. Подробности — README Buharestskie/contur_talk_widget.

Что делает

contur_talk_widget — связка amoCRM и Контур.Толка. Когда сделка переходит на выбранный этап воронки, сервис сам создаёт видеовстречу в Толке «Разбор с финансовым экспертом» на час, начиная со времени из поля сделки с датой встречи. Организатор — ответственный по сделке (если он найден среди пользователей Толка по имени и фамилии), иначе — организатор по умолчанию. Ссылка на встречу записывается в поле сделки, и менеджер видит её прямо в карточке.

Работает в одну сторону: из Толка в amoCRM ничего не возвращается. Разбор записей встреч — отдельный проект, kontur-talk-reporter.

Состояние

Сейчас сервис не работает нигде. Последние правки — июнь 2026. На старом сервере маршрут к нему был, но вёл в пустоту, и при переезде в сентябре 2026 его не перенесли. Встречи в Толке из сделок сейчас автоматически не создаются.

Если решат запустить

Лендинги

Лендинги

Квиз-лендинги

Для разработчиков. Состояние на 25.09.2026. Подробности, переменные и выкатка — README репозиториев Buharestskie/mfctest, do-nula-refinancing-2, bfl_fspp, mfcspisan. Что видит отдел продаж — статья «Заявки с лендингов».

Коротко

Четыре лендинга-квиза — лидогенераторы отдела продаж. Посетитель отвечает на несколько вопросов и оставляет телефон, лендинг заводит в amoCRM контакт и сделку, а три из четырёх — ещё и примечание с ответами (у do-nula-refinancing-2 примечания нет: ответы только в полях сделки). Своей бизнес-логики у них почти нет: сборка заявки, лимиты от спама и, у трёх из четырёх, код из SMS.

РепозиторийСайтЧто обещаетВоронка → этапПосле телефонаГде живёт
do-nula-refinancing-2реструктуризация.onlineреструктуризация долгов«Продажи МПП» → «Квалификация»код из SMSglobal
bfl_fsppfssp.siteпроверка долгов по базе ФССП (обращения к ФССП в коде нет)«Продажи МПП» → «Квалификация»код из SMSglobal
mfcspisanmfcspisan.ruсписание долгов через МФЦ«Продажи МПП» → «Квалификация»код из SMSсторонний сервер, доступа нет
mfctestmfctest.ruсписание долгов через МФЦ«МПП ИИ-агент» → «Новый лид Тест»ссылка в мессенджер, бот монолита donulaglobal

Родословная

Первым был do-nula-refinancing-2 (апрель 2026). bfl_fspp — его форк, mfcspisan — форк bfl_fspp: файлы бэкенда местами совпадают байт в байт, requirements.txt одинаковый у всех. mfctest стоит особняком: это лендинг pages/dolgimfc, вырезанный из монолита donula, с переписанным бэкендом. Аудит 11.09.2026 рекомендовал слить четыре лендинга в один параметризуемый — различаются набор вопросов, домен, воронка и тексты, ровно то, что выносится в конфиг. Не сделано.

Как устроен каждый

mfctest: две сделки

После телефона сделка «Лид с сайта» создаётся сразу, SMS нет. Потом посетитель выбирает мессенджер, ссылка ведёт в бота монолита donula с параметром ai-<id сделки>. Бот заводит свою сделку «ИИ Лид» в той же воронке и записывает строку ai-<id> в её поле; сделку лендинга он не трогает. Связь двух сделок — только эта строка.

Выкатка

На что наступают

Инфраструктура

Инфраструктура

Мониторинг и автоматизация серверов: infra

Для разработчиков. Состояние на 28.09.2026. Подробности, переменные ролей и команды — README репозитория infra и files/heartbeat/README.md. Как считается, кто и чем пользуется, — статья «Телеметрия использования: usage_telemetry».

Коротко

Репозиторий infra — единственный источник правды о серверах: инвентарь Ansible, роли базовой настройки, стек мониторинга, правила алертов, дашборды Grafana, статус-страница и служба окон обслуживания. Главное правило проекта: изменение, сделанное мимо репозитория, не существует. Накликанное на сервере или в веб-интерфейсе затрётся следующим прогоном роли.

ЧтоГде в репозиторииСнаружи
Prometheus, Alertmanager, blackbox_exporter, Pushgateway, Grafanaroles/monitoring_server, правила — prometheus/rules/Grafana — grafana.donula26.ru (вход по логину); остальное наружу не смотрит
Агент метрик на каждом сервереroles/monitoring_agent—
Метрики PostgreSQLroles/postgres_exporter—
Статус-страницаroles/status_pagestatus.donula26.ru
Окна обслуживания (админка и MCP)roles/status_adminstatus.donula26.ru/admin/, status.donula26.ru/mcp
Heartbeat периодических задачfiles/heartbeat/heartbeat.pypush.donula26.ru
GlitchTip (ошибки приложений)роли нет; проба и реестр проектов — docs/projects.mdmonitoring.donula26.ru
Бэкапы баз, фаервол, базовая настройка, аудитroles/db_backup, roles/firewall, roles/base, playbooks/audit.yml—
Коллектор телеметрии использованияroles/telemetry_collector—

Метрики и алерты

Alertmanager шлёт уведомления в Telegram через прокси (напрямую api.telegram.org недоступен), в два чата:

АлертКуда и когда
severity: criticalВ критический чат сразу, с 08:00 до 23:00 по Москве. Если сработал ночью, придёт в 08:00, при условии что проблема ещё держится. Повтор раз в час.
severity: warning + delivery: promptВ тот же чат, днём сразу, ночью придерживается до утра. Повтор раз в 6 часов. Метка для того, что не срочно, но нужно узнать в тот же день.
Остальные warning и infoУтренняя сводка: одно сообщение в окне 08:00–08:20.

Подавления: при HostDown молчит всё, что есть на этом хосте. При DockerDown и DockerMetricsStale молчат контейнерные алерты. При AmoDbPostgresDown молчат производные проверки amo_db. При DiskUsageCritical молчат предупреждения о том же диске. Истёкшие тишины Alertmanager хранит 30 дней: это журнал объявленных работ.

Дашборды Grafana лежат в папке «Инфраструктура»: «Инфраструктура: серверы», «Инфраструктура: контейнеры», «Доступность снаружи», «Периодические задачи», «amo_db», «Использование приложений». Их JSON генерируется скриптами generate.py и generate_usage.py в roles/monitoring_server/files/dashboards/.

Правка дашборда в браузере не сохранится: включено allowUiUpdates: false. Дашборды меняют в генераторе, потом прогоняют роль.

GlitchTip

GlitchTip на monitoring.donula26.ru собирает исключения приложений через SENTRY_DSN. Своя запись в нём есть у каждого нашего проекта. Одной переменной SENTRY_DSN мало: в коде нужны зависимость sentry-sdk и вызов инициализации. Проще всего взять init_sentry() из пакета usage_telemetry: install_telemetry() вызывает её сам.

Статус-страница

status.donula26.ru открыта без авторизации и сделана для сотрудников. Это не Uptime Kuma и не отдельный монитор: страницу генерирует скрипт roles/status_page/files/generate.py. Он запускается по таймеру раз в минуту, берёт из Prometheus ту же метрику probe_success, на которой построены алерты, и пишет статический HTML. Поэтому страница не может показывать не то, что пишут в чат алерты.

Список сервисов задаётся в status_page_services. Поля: probe — адрес пробы, как в monitoring_probe_targets; title — название, понятное сотруднику; visible — показывать ли на странице (инфраструктурные сервисы проверяются, но скрыты); project и job на странице не видны, по ним окно обслуживания глушит алерты. Внутренние имена, адреса и версии на страницу не попадают.

СостояниеКогда
Работаетпроба успешна, и за сутки вне окон обслуживания сбоев не было
С перебоямисейчас отвечает, но за сутки были сбои; или падает, но меньше двух минут подряд
Не работаетвсе пробы за последние 2 минуты неуспешны (status_page_down_window)
На обслуживаниисейчас идёт объявленное окно; перекрывает все остальные состояния
Нет данныхпо пробе нет метрик, например недоступен Prometheus

У каждого сервиса есть полоса за сутки из 48 получасовых сегментов и процент доступности за сутки. Минуты внутри окон обслуживания в процент не входят.

Окна обслуживания

Любой деплой, обновление или фикс сервиса, у которого есть внешняя проба, идёт в окне обслуживания. Одна запись об окне делает три вещи:

Порядок работ: открыть окно, выкатить и проверить, сразу закрыть окно. Пока окно открыто, мониторинг сервиса слепой, поэтому лучше открыть короткое окно и продлить его, чем забыть длинное. Причину пишут человеческим языком, её видит любой сотрудник: «Обновление дашборда», а не «деплой v2.3.1».

Через админку

  1. Открыть status.donula26.ru/admin/. Вход по именному логину: имя вошедшего записывается автором окна.
  2. Выбрать «Сервис», заполнить «Причина», проверить «Начало» и «Конец». По умолчанию окно идёт от текущего момента на 30 минут.
  3. Нажать «Объявить работы».
  4. После работ нажать «Закончить сейчас» в строке окна. Конец окна переносится на текущий момент, тишины снимаются, запись остаётся в истории.

Кнопка «Убрать» удаляет запись целиком, вместе с синим сегментом. Она нужна только для окон, заведённых по ошибке. В таблице видны окна за последние трое суток и все предстоящие.

Через MCP

Та же служба отвечает по MCP на https://status.donula26.ru/mcp. Имя сервера — status-maintenance, авторизация — заголовок Authorization: Bearer <токен>. Токен у каждого разработчика свой. Пример подключения в Claude Code:

claude mcp add --transport http status-maintenance https://status.donula26.ru/mcp \
  --header "Authorization: Bearer $STATUS_MCP_TOKEN"
ИнструментПараметрыЧто делает
list_maintenancehours (по умолчанию 72)идущие, предстоящие и недавние окна с их id
open_maintenanceservice, minutes (не больше 120), reason, необязательный start в ISO по Москвеоткрывает окно; по умолчанию оно начинается сейчас
close_maintenanceidзакрывает окно сейчас и снимает тишины

Доступы задаются в Ansible Vault: status_admin_web_users (логины админки) и status_admin_mcp_tokens (токены MCP), затем прогоняется playbooks/status_admin.yml. Чтобы отозвать доступ у одного человека, его строку удаляют и снова прогоняют роль. Остальные доступы продолжают работать.

Heartbeat

Проблема: Celery Beat или APScheduler умирают, не роняя контейнер. Процесс жив, healthcheck зелёный, проба отвечает 200, а задача не запускалась третьи сутки. Такое видит только сама задача, поэтому после каждого успешного прогона она отправляет отметку в Pushgateway.

Ansible: как выкатывать

На что наступают

Инфраструктура

Телеметрия использования: usage_telemetry

Для разработчиков. Состояние на 28.09.2026. Подробности и переменные — README репозитория usage_telemetry. Выкатка коллектора и дашборд лежат в репозитории infra, про него — статья «Мониторинг и автоматизация серверов: infra».

Коротко

usage_telemetry отвечает на вопрос, какими приложениями и какими их разделами пользуются живые люди. Цель — отличить приложение, которое висит мёртвым грузом, от приложения, которым пользуются редко, но по делу, и выключать только первое. Проект состоит из двух частей:

Результат смотрят в Grafana, на дашборде «Использование приложений».

приложение (middleware) ──пачками по HTTP──> коллектор ──> PostgreSQL ──> Grafana

Как подключить к проекту

  1. Добавить пакет в requirements.txt (или в pyproject.toml), закрепив тег:
    usage-telemetry @ https://github.com/Buharestskie/usage_telemetry/archive/refs/tags/v0.2.0.tar.gz
    Для отправки ошибок в GlitchTip нужен ещё sentry-sdk (extra sentry).
  2. Повесить сбор на приложение FastAPI или Starlette:
    from usage_telemetry import install_telemetry
    
    app = FastAPI()
    install_telemetry(app)
    Если задан SENTRY_DSN, install_telemetry заодно инициализирует GlitchTip. Если в проекте SDK уже поднят своим кодом, вызывайте install_telemetry(app, sentry=False).
  3. Сообщить, кто пришёл. В зависимости авторизации, где сессия или токен уже разобраны:
    from usage_telemetry import mark_user
    
    mark_user(request, user_id=session.amocrm_user_id, auth="session", session_id=session_id)
    Значения auth: session — человек в браузере; jwt — виджет amoCRM с токеном от widget_auth; telegram — бот или Mini App; basic — админки; api_key — вызов между сервисами; none — без авторизации. Без mark_user вид входа угадывается по заголовкам, но идентификатора пользователя не будет.
  4. Для бота на aiogram 3:
    from usage_telemetry import install_bot_telemetry
    
    install_bot_telemetry(dispatcher)
  5. Добавить переменные в compose проекта и выкатить в окне обслуживания.
ПеременнаяОбязательнаЧто означает
TELEMETRY_SERVICEдамашинный код приложения. Должен совпадать с project этого сервиса в status_page_services (infra)
TELEMETRY_URLдаадрес коллектора. Приложения на одном сервере с коллектором используют http://host.docker.internal:8130, с других серверов — https://donula26.ru/telemetry
TELEMETRY_ENABLEDнет0 выключает сбор целиком
TELEMETRY_QUEUE_SIZE, TELEMETRY_BATCH_SIZE, TELEMETRY_FLUSH_SECONDS, TELEMETRY_TIMEOUTнеточередь (5000), размер пачки (200), интервал отправки неполной пачки (5 с), таймаут запроса (3 с)
SENTRY_DSNнетпроект в GlitchTip; без него ошибки не отправляются

Если не задан TELEMETRY_SERVICE или TELEMETRY_URL, пакет пишет предупреждение в лог и ничего не собирает. Приложение при этом работает как обычно.

Что собирается

Одно событие на одно обращение к приложению:

ПолеЧто в нём
at, service, sourceвремя, код приложения, источник: http или bot
method, pathметод и шаблон пути из маршрута (/api/deals/{id}, а не /api/deals/451). Если маршрута нет, путь нормализуется: числа заменяются на {id}, UUID — на {uuid}, длинные непрозрачные сегменты — на {token}
status, duration_msкод ответа и длительность
auth, user_id, session_idчем запрос аутентифицирован и чей идентификатор из уже существующей авторизации: amocrm_user_id, id из JWT, telegram_id
user_agentпервые 200 символов
client_ipдоходит только до коллектора. Там из него, user-agent и общей соли считается отпечаток посетителя, а сам адрес не сохраняется

Как устроено внутри

Где смотреть

Grafana, grafana.donula26.ru → папка «Инфраструктура» → дашборд «Использование приложений». Период по умолчанию — 30 дней.

ПанельЧто показывает
Приложений под наблюдениемсколько приложений в справочнике
Сбор не раскатанприложения, от которых не пришло ни одного события. Пока здесь не ноль, выводы о неиспользуемых делать рано
Без единого обращения за периодсбор работает, а обращений не было — кандидаты на выключение
Приложения по живостиглавная таблица: обращения, люди, посетители (оценка), человеко-дни, последний заход и замечание: «сбор не подключён», «никто не заходил», «люди не опознаются» или «только оценка по отпечатку»
Человеко-дни по приложениямграфик по дням
Разделы и данныеразделы выбранного приложения (переменная «Приложение»)
Кто пользуетсяидентификаторы пользователей выбранного приложения, без имён: нужны, чтобы предупредить людей перед выключением
Медленное и ошибающеесяразделы с ошибками или с 95-м перцентилем больше 3 секунд

Дашборд генерируется скриптом roles/monitoring_server/files/dashboards/generate_usage.py в infra. В браузере он не редактируется.

Выкатка

На что наступают

Внутренние сервисы

Внутренние сервисы

SMS-шлюз: beeline-service

Для разработчиков. Состояние на 28.09.2026. Переменные, все ручки и выкатка по шагам — README репозитория Buharestskie/beeline-service. Как лендинги работают целиком — статья о квиз-лендингах в этой же книге.

Коротко

beeline-service — общий шлюз исходящих SMS. Сервис присылает номер и готовый текст, шлюз проверяет, что номер российский мобильный, применяет антиспам-лимиты и отдаёт сообщение Билайну (A2P) или хабу МегаФона. Шаблонов у шлюза нет: текст целиком приходит от вызывающего. Снаружи шлюз доступен по адресу https://donula26.ru/service/beeline, пускает только адреса из своего списка доступа.

Кто вызывает

Сейчас — три квиз-лендинга, которые подтверждают телефон посетителя кодом из SMS («Ваш код подтверждения: 1234»):

ЛендингЧто передаёт
do-nula-refinancing-2phone, message, ip посетителя. lead_id не передаёт никто
bfl_fspp
mfcspisan (на стороннем сервере)

mfctest шлюз не вызывает: SMS-подтверждения у него нет, после телефона посетитель уходит в мессенджер. Все вызывающие ходят через nginx по публичному адресу, поэтому адрес каждого нового вызывающего нужно заранее внести в ALLOWED_CALLER_CIDRS, иначе он получит 403 Caller IP is not permitted.

Как устроен

Билайн или МегаФон

SMS уходит в хаб МегаФона, только если выполнены оба условия:

  1. на странице настроек шлюза включён переключатель «Использовать сервис Мегафон»;
  2. номер по плану нумерации выделен МегаФону: таблица диапазонов лежит в app/services/megafon_ranges.json (25 DEF-кодов) и читается один раз при старте.

Всё остальное идёт через Билайн. По умолчанию переключатель выключен, то есть без ручного включения все SMS уходят Билайну.

Как вызвать

curl -X POST https://donula26.ru/service/beeline/sms/send \
  -H 'Content-Type: application/json' \
  -d '{"phone": "+7 000 000-00-01", "message": "Ваш код подтверждения: 4821", "ip": "<ip посетителя>"}'
ПолеОбязательноЧто это
phoneдаРоссийский мобильный в любом написании, 5–32 символа
messageдаТекст, 1–1000 символов, уходит провайдеру как есть
ipнетАдрес посетителя. Без него не действуют лимит повтора и лимит номеров на IP
lead_idнетСделка amoCRM для примечания об SMS

Проверка живости — GET /sms/health, открыта без списка доступа, отвечает {"status":"ok"}.

Где код и как выкатывается

На что наступают

Внутренние сервисы

Воронка найма: hiring_project

Для разработчиков. Состояние на 28.09.2026. Переменные, все ручки, порядок выкатки и отката — README и RUNBOOK репозитория Buharestskie/hiring_project.

Коротко

hiring_project — воронка найма менеджеров по продажам («охотников») для самой компании: от входа кандидата на сайт до выхода на обучение. Кандидат входит через Telegram, VK или MAX, заполняет анкету и записывает голосовой ответ. Голос оценивает Gemini, решение принимает РОП прямо в Google Sheet, дальше кандидату приходит сообщение в мессенджер, приглашение на групповое собеседование в Телемосте и ссылка на онлайн-обучение. В amoCRM кандидат попадает только после собеседования.

Сейчас сервис на наших серверах не развёрнут: проект передан новому владельцу и ждёт переезда. Домен hiring.donula.online, заложенный в целевую схему, пока не настроен. Внешней пробы в мониторинге нет.

Путь кандидата

  1. Вход. Сайт запрашивает одноразовый токен (/api/auth/token, живёт 10 минут) и открывает бота в выбранном мессенджере. Кандидат отправляет боту телефон, сайт опрашивает /api/auth/check и получает имя и телефон из мессенджера, а не из формы.
  2. Анкета. /api/submit принимает анкету и голос (audio/webm, audio/mp4, audio/m4a, от 1 KiB до 10 MiB). Анкета сначала пишется в журнал на диске, затем строкой в лист «Кандидаты» через Apps Script. Повтор с тем же request_id вторую строку не создаёт.
  3. Оценка. Бэкенд отправляет голос в Gemini (через ProxyAPI): пять критериев от 1 до 5. 20 баллов и выше — PASS, кандидату сразу уходит приглашение на собеседование; 10–19 — GRAY, решает РОП; меньше 10 — FAIL, кандидату уходит отказ. Если модель недоступна, ставится GRAY с пометкой «AI недоступен, слушайте вручную».
  4. Решение РОПа — в таблице: столбец T («Одобрен» / «Отклонён») и W — итог собеседования («Принят» / «Отклонён» / «Не пришел»). Apps Script складывает изменения в лист «Webhook Outbox» и доставляет их в бэкенд подписанным запросом на /api/webhook/rop; бэкенд пишет кандидату.
  5. Собеседование — ближайший понедельник, среда или пятница не раньше чем через сутки, в 12:00 по Москве, в Телемосте. Напоминания — за 24 часа и за час.
  6. Обучение. После «Принят» создаются контакт и сделка Найм: <имя> в воронке найма amoCRM, кандидат получает персональную ссылку на курс (живёт 90 дней). Сайт обучения отмечает визит и прохождение, сделка двигается только вперёд: принят → зашёл на обучение → готов к ролевке. После курса открывается ссылка в закрытое сообщество.

Как устроен

Где код и как запустить

На что наступают

Интеграции

Интеграции

Отчёты по встречам Контур.Толка: contur_transcribe

Для разработчиков. Состояние на 28.09.2026. Переменные, песочница и выкатка по шагам — README репозитория Buharestskie/contur_transcribe. На сервере проект развёрнут под именем donula-kontur-reporter.

Коротко

contur_transcribe — демон без интерфейса и без входящих запросов. Раз в пять минут он забирает новые записи встреч из пространства Контур.Толка, получает расшифровку, делает управленческий разбор языковой моделью и раскладывает результат: короткий отчёт и файл расшифровки — в закрытую группу Telegram, структурированные поля — в рабочую таблицу Google Sheets отдела продаж. Задача — чтобы итоги тет-а-тетов и общих встреч РОПов доходили до таблицы без ручной работы.

Как устроен один проход

  1. Список записей. Один ключ пространства (KTALK_SPACE_API_KEY) читает записи всех РОПов за последние LOOKBACK_DAYS дней (по умолчанию 7). Уже опубликованные и тестовые (по шаблонам названий) пропускаются.
  2. Расшифровка. Сначала берётся готовая расшифровка Толка. Если текста в ней нет, запись скачивается в качестве 240p, из неё вытаскивается звук (ffmpeg) и отправляется в сервис распознавания a5x; задача опрашивается до готовности (до часа). Если у Толка запись ещё не готова, встреча откладывается до следующего прохода — это не ошибка.
  3. Разбор. Диалог уходит в языковую модель a5x (A5X_MODEL). Она возвращает короткий управленческий радар: план и факт отдельно по каждому сотруднику, одна строка по каждому участнику, до трёх главных проблем, все поставленные задачи с ответственными и сроками (до 12), где нужен руководитель, оценка полезности встречи 0–10. Отсутствующее помечается «Не зафиксировано». Длинные встречи разбираются через конспекты частей. Если модель недоступна, разбор собирается из протокола Толка.
  4. Маршрутизация. Если участников два-три, однозначно опознан один сотрудник и на встрече был его РОП — это тет-а-тет: заполняется строка сотрудника на листе этого РОПа. Всё остальное — на лист «Общие встречи». Полная расшифровка в любом случае пишется на скрытый лист «Расшифровки Контур.Толк».
  5. Telegram. Отчёт одним сообщением (длинный — разбивается на части) и расшифровка отдельным .txt.
  6. Метрики. В конце прохода счётчики contur_transcribe_cycle_* (найдено, опубликовано, отложено, упало, пропущено) уходят в Pushgateway мониторинга.

Каждый выполненный шаг сразу отмечается в data/state.json. Сбой посередине не теряет встречу и не публикует её дважды: следующий проход доделает только оставшееся.

Кто есть кто

Сотрудники и РОПы описаны в двух справочниках вне кода: config/rops.json (РОП и название его листа) и config/employees.json (сотрудник, его РОП и признаки опознания: emails, konturUserKeys, aliases, roomNames, titlePatterns). Образцы — *.example.json. Заполнять руками не нужно: python -m tools.sync_identities показывает, что изменится, с --write — дописывает каждому найденному человеку ключ, почту и форму имени из справочника пространства.

Мониторинг

Проба снаружи сервису бессмысленна: у него нет адреса. Вместо неё два правила в prometheus/rules/contur_transcribe.yml репозитория infra:

АлертКогдаОбычная причина
ConturTranscribeStalled30 минут подряд: встречи находятся, часть падает, опубликовано нольотказ распознавания или модели a5x
ConturTranscribeSilentпроходов не было больше 30 минутконтейнер упал, завис или не поднялся

Метрики уходят, только если задан METRICS_PUSH_URL (с ним — METRICS_PUSH_USER, METRICS_PUSH_PASSWORD, METRICS_INSTANCE); без адреса отправка молча выключена. Здоровое состояние без новых встреч выглядит как «найдено 59, пропущено 59, опубликовано 0» — пропущенные в основном уже опубликованы раньше.

Где код и как запустить

На что наступают

Интеграции

YouGile и GitHub: yogile_connector

Для разработчиков. Состояние на 28.09.2026. Переменные и все ручки — README репозитория Buharestskie/yogile_connector.

Коротко

yogile_connector связывает задачи YouGile с ветками GitHub и двигает задачу по доске вслед за кодом: ветки нет — задача ждёт ветку; ветка появилась — задача в работе; ветка слита в основную — задача закрыта; после слияния пришли новые коммиты — задача снова в работе. Внутри YouGile скрипт-витрина показывает состояние бейджем на карточке и блоком «GitHub» в открытой задаче. Сервис доступен по адресу https://dev-tasks.donula26.ru, настройки — в админ-панели /admin.

Как задача находит свои ветки

Что сервис делает сам

СобытиеЧто происходит
Задачу перенесли в рабочую колонкуСоздаётся ветка в репозитории из стикера (если на доске включено автосоздание)
Для задачи появилась веткаЗадача переезжает в рабочую колонку
Все ветки слитыЗадача уезжает в «Готово» и отмечается выполненной
После слияния пришли новые коммитыЗадача возвращается в работу
Задача ушла из рабочей колонкиПустая ветка (без своих коммитов) удаляется — кроме колонок, где ветки велено сохранять
Задача закрытаСлитая ветка удаляется. Неслитая не удаляется никогда — в чат задачи один раз приходит сообщение с её именем и числом неслитых коммитов

Автосоздание ветки на доске по умолчанию выключено, удаление пустых и слитых веток — включено; всё это переключается по доскам в админ-панели. Каждый переход пишется в чат задачи, если задан YOUGILE_BOT_USER_ID. Цвет карточки меняется под состояние: серый — нет репозитория или ветки, зелёный — слито.

Откуда события

Админ-панель

Настройки живут в базе (SQLite в ./data по умолчанию) и правятся на /admin: репозиторий и ключи по умолчанию, параметры YouGile, таблица досок — какие синхронизировать, какой репозиторий у каждой, какие колонки рабочая и завершающая, создавать ли ветку автоматически. При первом запуске значения переносятся из окружения, дальше окружение нужно только для базы и токенов входа. Кнопки панели ходят в /admin/api/* с заголовком X-Admin-Token:

РучкаЧто делает
POST /admin/api/reconcileПолная сверка досок
POST /admin/api/webhooks/yougileСоздать подписки YouGile, тело {"public_url": "https://dev-tasks.donula26.ru"}
POST /admin/api/webhooks/githubЗавести вебхук сервиса во всех репозиториях (или в переданном списке repos)
POST /admin/api/reloadПеречитать настройки и структуру досок
GET /admin/api/statusСводка состояния

Проверка живости — GET /healthz, отвечает {"status":"ok"}.

Где код и как выкатывается

На что наступают

Продукты

Продукты

Личный кабинет Юмобайл: web-app-umoible

Для разработчиков. Состояние на 28.09.2026. Переменные, ручки и выкатка подробно — в README репозитория Buharestskie/web-app-umoible; карта «экран → запрос → поля» — API-MAP.md там же.

Коротко

Личный кабинет абонента Юмобайла (MVNO на сети билайн): баланс, тариф, остатки, статус номера, несколько номеров на одном аккаунте, пополнение через СБП и картой, новости, чат с поддержкой, уведомления и активация новой SIM — от скана серийника до подписания договора в Госключе или проверки паспорта. Данные абонента — не наши: их отдаёт API оператора, приложение показывает ровно то, что он вернул. Своё у нас — два бэкенда: чат и уведомления, и выдача номеров при активации.

ЧастьПапкаЧто этоЖивое?
Веб-кабинет (PWA)web/React 19 + TypeScript + Vite, открывается на app.umobile.ru/app/, ставится на экран «Домой»да, основная форма продукта
Чат и уведомленияchat-backend/FastAPI + Postgres: чат с оператором через amoCRM, лента уведомлений, web-push и FCM, ведение сделки активации в amoCRMда
Сервис активацииactivation-admin/FastAPI + Postgres: пул номеров и партии SIM, выдача номера при скане, привязка в ЛК оператора, сессии, админкада
Нативная оболочка iOSnative/SwiftUI + WKWebView; нативные чат, сканер SIM, пуши через Firebaseзаморожена с 26.08.2026, см. ниже
Конфиги nginxserver/справочные копии боевых конфигов обоих доменов и зоны микрокэшасправочно
Выкаткаdeploy/, .github/workflows/ci.ymlскрипт выкатки и CIда
Документациякореньlk_umobile_api.yaml (спека API оператора), API-MAP.md, SPEC-*.md и TZ-*.md — задачи для разработчика API оператора, ПИСЬМО-АВТОРУ-API*.mdсправочно

AGENT_CONTEXT.md местами описывает прежний фронт (оболочка app.html с экранами в iframe). Его убрали из дерева, он лежит под тегом legacy-frontend. Всё про экраны сверяйте с web/src.

Откуда данные и куда уходят

С какими нашими сервисами связан

Как устроено

Веб-кабинет

Активация SIM

  1. Контактный номер → код из SMS. Код шлёт и проверяет ЛК оператора (регистрация по коду), в amoCRM заводится сделка.
  2. Скан серийника пустышки. Сервис активации бронирует номер из пула на 30 минут, берёт у чат-бэкенда id сделки, привязывает номер в ЛК. Пул разделён на B2C и B2B, пропорция выдачи настраивается в админке.
  3. Оплата, дальше развилка по сегменту номера: B2C подписывает договор через Госуслуги и Госключ, B2B снимает паспорт своей камерой с рамкой (/activation/kyc).
  4. На каждом шаге фронт двигает сделку по этапам воронки (POST /api/v1/activation/stage).

Статусы в базе активации. Номер: pool (свободен), res (бронь), activ (активируется), act (активен), taken (уже занят в ЛК), blk, quar (карантин). SIM: ship → pay → prov → bound, отдельно blk. Каждое движение номера пишется в журнал number_log, в админке он открывается щелчком по номеру или ICCID. Сама таблица numbers хранит только текущее состояние.

Нативная оболочка

Приложение native/ грузит не React-сборку, а свою офлайн-копию прежнего фронта (native/Yumobile/app.html и screens/). Чат, регистрацию пуш-токена и часть API берёт с app.umobile26.ru. Собирается скриптом native/build-install.sh с бесплатной подписью разработчика. Правки в web/ в нативку не попадают.

Авторизация в общих словах

Переменные окружения

Значения — в .env рядом с docker-compose.yml каждого бэкенда, в git их нет. Полный список чата — chat-backend/config.py.

СервисПеременные
chat-backendDATABASE_URL, AMO_SUBDOMAIN, AMO_CLIENT_ID, AMO_CLIENT_SECRET, AMO_REDIRECT_URI, AMOJO_CHANNEL_ID, AMOJO_CHANNEL_CODE, AMOJO_CHANNEL_SECRET, APP_SECRET, SESSION_SECRET, NOTIFY_SECRET, LK_INTERNAL_KEY, MAGIC_SECRET, MAGIC_WORKER, VAPID_PRIVATE_PATH, VAPID_SUBJECT, FCM_PROJECT_ID, FCM_CREDENTIALS_PATH, PUBLIC_BASE_URL, WORKERS
activation-adminDATABASE_URL, ADMIN_KEY, APP_KEY, SESSION_SECRET, LK_BASE, LK_INTERNAL_KEY, CHAT_BASE, APP_SECRET, OTP_TEST_MODE
web (при сборке)VITE_APP_SECRET, VITE_APP_KEY, VITE_VAPID_PUBLIC_KEY — необязательные, без них берутся значения из web/src/shared/config.ts

Ссылка с автовходом

В чат-бэкенде есть догон тех, кто бросил активацию на середине. На вебхук amoCRM о новой сделке бэкенд выпускает короткую ссылку app.umobile.ru/i/<код> и пишет её в поле сделки «Ссылка на ЛК». По ссылке абонент попадает в кабинет уже вошедшим, без ввода номера и кода, а главная сама показывает следующий шаг. Код гасит не переход по ссылке, а запрос из приложения — так превью-боты мессенджеров не сжигают ссылку раньше человека. Фоновая рассылка напоминаний включается переменной MAGIC_WORKER, по умолчанию выключена. По SPEC-magic-link.md 03.09.2026 рассылку приостановили до ответа автора API оператора. Что включено сейчас, смотреть в .env на сервере.

Локальный запуск

Выкатка

На что наступают

Продукты

WebRTC-телефония: pbx

Для разработчиков. Состояние на 28.09.2026. Подробности, полный список ручек и переменных — README репозитория Buharestskie/pbx. Кода самого виджета amoCRM в репозитории нет — только его серверная часть.

Коротко

pbx — собственная WebRTC-телефония отдела продаж на Asterisk. Главное в ней — автодозвонщик: менеджер ставит статус «Готов», система берёт из очереди следующий лид, звонит сначала менеджеру, после его ответа — клиенту, и соединяет их. Менеджер разговаривает из браузера: виджет в amoCRM или приложение-софтфон. После звонка в сделке появляется примечание со ссылкой на запись.

Лиды попадают в очередь из amoCRM вебхуками этапов: сделка перешла на этап — контакт встал в кампанию. Руководители настраивают кампании, операторов и смотрят дашборды в админке.

Из чего состоит

КаталогЧто это
backend/Один процесс на Node.js 20 (ES-модули): HTTP API на Express (порт API_PORT, 3000), WebSocket для виджетов (WS_PORT, 3001), клиент Asterisk ARI (ari-client), свой клиент AMI (только для стерео-записи), PostgreSQL (pg). Там же фоновые задачи: ready-тик раз в 10 с, воркер вебхуков, сторожа зависших контактов и каналов, перевод из затянувшейся постобработки в «Паузу», отправка статусов менеджеров наружу.
admin/Админка — статические HTML-страницы: кампании, операторы, группы, транки, график работы, история звонков, дашборды. Ходят в /api/admin/… по относительному пути. Вкладка «Трансфер» — пока статичный макет без запросов к API.
leadboard/Одна страница: таймлайн работы с лидами МПП-2 (/api/admin/dashboards/lead-timeline).
guide/Инструкции для операторов и заметки о релизах виджета. Чистая статика, к API не обращается.
scripts/Серверные скрипты по cron: бэкап (pbx-backup.sh), разбор громкости свежих записей (analyze-recordings.sh), генератор отчёта о постобработках (regen-wrapup-report.py, его же запускает кнопка в админке).

Статику backend не раздаёт: в Express её нет, страницы отдаёт веб-сервер перед ним. Скрипты в корне backend/ (checkleads*.mjs, chat-*.js, reconcile.mjs и подобные) — разовые сверки, в работе сервиса не участвуют.

Чего в репозитории нет: исходников виджета amoCRM, софтфона для Mac/Windows и расширения браузера, конфигов Asterisk (pjsip, диалплан, звуки опроса) и coturn. Бэкенд только генерирует из базы SIP-аккаунты операторов и клиентов приложения и перезагружает pjsip.

Путь звонка

  1. Лид в очереди. amoCRM шлёт вебхук этапа на /api/admin/webhook/<токен кампании>. Ручка только кладёт id сделки в таблицу webhook_queue. Воркер раз в 2 с берёт до 5 строк, тянет сделку и телефон контакта из amoCRM и добавляет контакт в кампанию.
  2. Менеджер «Готов». Виджет держит WebSocket с бэкендом (auth, agent_status, sip_registered…) и SIP-регистрацию в Asterisk через WebRTC. Звонить начинают только после sip_registered.
  3. Выбор контакта. PL/pgSQL-функция pick_next_contact атомарно выдаёт контакт (FOR UPDATE SKIP LOCKED) с учётом приоритета кампаний, закрепления за менеджером, окна звонков по часовому поясу клиента и циклов повторов.
  4. Звонок менеджеру. Виджету уходит screen_pop с карточкой лида, через 300 мс — ARI originate на PJSIP/<extension> в Stasis-приложение (STASIS_APP, по умолчанию pbx-dialer) с аргументами agent,<callId>.
  5. Менеджер ответил (событие StasisStart с ролью agent) — виджету dialing_client, статус менеджера становится oncall, менеджер назначается ответственным за сделку в amoCRM (кроме нескольких воронок-исключений в коде). Затем originate клиенту через транк: по умолчанию шаблон OUTBOUND_ENDPOINT_TEMPLATE, у оператора может быть свой транк.
  6. Клиент ответил (StasisStart с ролью client) — оба канала в mixing-bridge, виджету connected, стартует запись: стерео через AMI MixMonitor, при сбое — моно через ARI.
  7. Конец разговора (StasisEnd) — строка в call_log, примечание-звонок в сделку amoCRM со ссылкой на запись, виджету call_ended; если клиент ответил, менеджер уходит в постобработку. Если трубку первым положил менеджер, а разговор длился не меньше RATING_SURVEY_MIN_TALK_SEC (40 с), клиента оставляют на линии и просят оценить разговор цифрой 1–5 (включается RATING_SURVEY_ENABLED=1, оценки — в call_ratings).

Если SIP менеджера отвалился посреди звонка, есть восстановление: 15 с на переподключение и повторный originate. Состояние активных звонков живёт только в памяти процесса.

Другие пути через тот же диалер:

Входящих звонков в диалере нет: StasisStart без известной роли игнорируется.

Откуда данные и куда уходят

Связи с нашими сервисами:

СервисКак связан
mpp2-touchespbx берёт у него rr-credits для выбора клоузера, сообщает о назначении менеджера и о смене статусов (MPP2_TOUCHES_BASE)
mpp2-tasks-widgetполучает смену статусов менеджеров (MPP2_TASKS_STATUS_URL); сам ставит статус «Готов» / «Офф» через POST /api/widget/agent-status и читает график работы
dashboard, dashboard_mpp2, SpeechApiзабирают историю звонков из /api/history — API в формате команды history Мегафон ОАТС (EXTERNAL_API_TOKEN)
webhook_for_call_recordскачивает записи по ссылкам из примечаний amoCRM
бэкенд мобильного приложения, app.donula26.ruзаводит клиентам SIP-аккаунты в pbx и получает от pbx запрос на push о входящем (APP_BACKEND_URL, APP_PROVISION_SECRET)

Статусы всех менеджеров отдаются в GET /api/manager-statuses (MANAGER_STATUS_TOKEN): ready «Готов», oncall «Занят», pause «Пауза», wrapup «Постобработка», learning «Обучение», offline «Офф».

Где код

Локальный запуск

  1. Нужны Node.js 20+ и PostgreSQL. Asterisk не обязателен: без ARI backend пишет Running in offline mode (no Asterisk), поднимает HTTP и WebSocket, но звонить не может.
  2. Схема базы — только дампом схемы с прода (как снять — в README): миграции её с нуля не создают. Из settings удалить токены amoCRM, если локальный процесс не должен ходить в боевой amoCRM.
  3. cd backend && cp .env.example .env, задать PG_*, любое значение ASTERISK_AMI_PASSWORD и локальные пути PJSIP_AGENTS_PATH / PJSIP_APP_CLIENTS_PATH.
  4. npm install, npm run migrate, npm start, проверить curl -s localhost:3000/health — ответ {"status":"ok","asterisk":"offline",…}.
  5. WebSocket — любым клиентом на ws://localhost:3001, первым сообщением {"type":"auth","agent_id":…}. Страницы admin/ и leadboard/ нужно открывать с того же адреса, что и API.

Выкатка

На что наступают