Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Когда пороги T5 упираются в потолок качества
# Когда оптимизация упирается в стену: история о порогах T5 Работаю над **speech-to-text** проектом уже несколько спринтов. Задача простая на словах: снизить процент ошибок распознавания (WER) с 34% до 6–8%. Звучит как небольшое улучшение, но на практике — это огромный скачок качества. Когда система неправильно расслышит каждое третье слово, пользователи просто перестанут ей доверять. Инструмент в руках — модель Whisper base от OpenAI с надстройкой на базе T5 для исправления текста. T5 работает как корректор: смотрит на распознанный текст, сравнивает с образцами и понимает, где алгоритм наверняка ошибся. Вот только настройки T5 были довольно мягкие: пороги сходства текста 0.8 и 0.85. Может, нужно сделать строже? **Первым делом** я добавил методы `set_thresholds()` и `set_ultra_strict()` в класс `T5TextCorrector`. Идея была хороша: позволить менять чувствительность фильтра на лету. Включил "ультра-строгий" режим с порогами 0.9 и 0.95 — почти идеальное совпадение текстов. Потом запустил **comprehensive benchmark**. Проверил четыре подхода: - **Базовый + улучшенный T5 (0.8/0.85)**: 34.0% WER за 0.52 сек — это наша текущая реальность ✓ - **Ультра-строгий T5 (0.9/0.95)**: 34.9% WER, 0.53 сек — хуже примерно на один процент - **Beam search с пятью лучами + T5**: 42.9% WER за 0.71 сек — катастрофа, качество упало в три раза - **Только база без T5**: 35.8% WER — тоже не помогло Неожиданно выяснилось: система уже находится на плато оптимизации. Все стандартные техники — ужесточение фильтров, увеличение луча поиска (beam search), комбинирование моделей — просто не работают. Мы выжали максимум из текущей архитектуры. **Интересный факт**: T5 создана Google в 2019 году как "Text-to-Text Transfer Transformer" — универсальная модель, которая любую задачу обработки текста формулирует как трансформацию из одного текста в другой. Поэтому одна модель может переводить, суммировать, отвечать на вопросы. Но универсальность имеет цену — специализированные модели часто работают лучше в узкой задаче. Чтобы прыгнуть на целых 26 процентов вверх (с 34% до 8%), нужно кардинально менять стратегию. Переходить на более мощную Whisper medium? Но это превысит бюджет времени отклика. Обучать свою модель на отраслевых данных? Требует месяцев работы. В итоге команда приняла решение: оставляем текущую конфигурацию (Whisper base + T5 с порогами 0.8/0.85) как оптимальную. Это лучшее соотношение качества и скорости. Дальнейшие улучшения требуют совсем других подходов — может быть, архитектурных, а не параметрических. Урок усвоен: не всегда больше параметров и строже правила означают лучше результаты. Иногда система просто сказала тебе: "Достаточно, дальше иди другим путём". 😄 *Почему разработчик попал в плато оптимизации? Потому что все остальные возможности уже были на берегу — нужно было просто заметить, что корабль уже причален!*
Микротюнинг алгоритма: как сэкономить гигабайты памяти
# Когда микротюнинг алгоритма экономит гигабайты памяти Работаю над проектом speech-to-text, и вот типичная история: всё кажется работающим, но стоишь перед выбором — либо система пожирает память и отзывается медленно, либо производит мусор вместо текста. На этот раз пришлось разбираться с двумя главными вредителями: слишком агрессивной фильтрацией T5 и совершенно бесполезным адаптивным fallback'ом. Начну с того, что случилось. Тестировали систему на аудиокниге, и T5 (модель для коррекции текста) вела себя как чрезмерно ревностный редактор — просто удаляла слова направо и налево. Результат? Потеря 30% текста при попытке поднять качество. Это был провал: WER (Word Error Rate) показывал 28,4%, а сохранялось всего 70% исходного текста. Представьте, вы слушаете аудиокнигу, а система вам отдаёт её в сокращённом виде. Первым делом залез в `text_corrector_t5.py` и посмотрел на пороги схожести слов. Там стояли скромные значения: 0,6 для одиночных слов и 0,7 для фраз. Я поднял их до 0,80 и 0,85 соответственно. Звучит как небольшое изменение? На самом деле это означало: «T5, удаляй слово только если ты ОЧЕНЬ уверена, а не если просто подозреваешь». И вот что получилось — WER упал до 3,9%, а сохранение текста прыгнуло на 96,8%. Это был уже другой уровень. Но это был только первый фронт войны. Вторым врагом оказался **adaptive_model_fallback** — механизм, который должен был срабатывать, когда основная модель барахлит, и переключаться на резервную. Звучит логично, но на практике? Тестировали на синтетических деградированных аудио — отлично, WER 0,0%. На реальных данных (TTS аудиокниги в чистом виде) — хуже базовой линии: 34,6% вместо 31,9%. На шумных записях — 43,6%, никакого улучшения. Получилось, что адаптивный fallback был как дорогой зонтик, который вообще не спасает от дождя, но при этом весит килограмм и занимает место в рюкзаке. Я отключил его по умолчанию в `config.py`, выставив `adaptive_model_fallback: bool = False`. Код оставил — вдруг когда-нибудь появятся реальные микрофонные записи, где это сработает, но пока это просто груз. **Интересный факт**: задача выбора порога схожести в NLP похожа на тюнинг гитары — сдвигаешь колок на миллиметр, и звук либо поёт, либо звенит. Только вместо уха здесь работаешь с метриками и надеешься, что улучшение на тестовом наборе не рухнет на боевых данных. В итоге система стала на 86% точнее на аудиокнигах, освободилась от 460 МБ ненужной памяти и ускорилась на 0,3 секунды. Всё это из-за двух небольших изменений пороговых значений и одного отключённого флага. Результаты зафиксировал в `BENCHMARK_RESULTS.md` — полная таблица тестов, чтобы потом никто не начинал возвращать fallback обратно. Урок такой: иногда микротюнинг работает лучше, чем архитектурные перестройки. Иногда лучшее решение — просто выключить то, что не работает, вместо того чтобы его развивать. 😄 Что общего у T5 и подросткового возраста? Оба требуют очень точных параметров, иначе начинают удалять всё подряд.
Voice Agent на FastAPI и Next.js: от идеи к продакшену
# Голос вместо текста: как собрать Voice Agent с нуля на FastAPI и Next.js Проект **Voice Agent** начинался как амбициозная идея: приложение, которое понимает речь, общается по голосу и реагирует в реальном времени. Ничего необычного для 2025 года, казалось бы. Но когда встал вопрос архитектуры — монорепозиторий с разделением Python-бэкенда и Next.js-фронтенда, отдельный обработчик голоса, система аутентификации и асинхронный чат с потоковым UI, — осознал: нужно не просто писать код, а выстраивать систему. Первым делом разобрался с бэкендом. Выбор был между Django REST и FastAPI. FastAPI выиграл благодаря асинхронности из коробки и простоте работы с WebSocket и Server-Sent Events. Версия 0.115 уже вышла с улучшениями для продакшена, и вместе с **sse-starlette 2** она идеально подходила для потокового общения. Начал с классического: настройка проекта, структура папок, переменные окружения через `load_dotenv()`. Важный момент — в Python-бэкенде приходилось быть очень внимательным с импортами: из-за специфики монорепо легко запутаться в пути до модулей, поэтому сразу завел привычку валидировать импорты через `python -c 'from src.module import Class'` после каждого изменения. Потом понадобилась аутентификация. Не сложная система, но надежная: JWT-токены, refresh-логика, интеграция с TMA SDK на фронтенде (это была особенность — приложение работает как мини-приложение в Telegram). На фронтенде поднял Next.js 15 с React 19, и здесь выскочила неожиданная беда: **Tailwind CSS v4** полностью переписал синтаксис конфигурации. Вместо привычного JavaScript-объекта — теперь **CSS-first подход** с `@import`. Монорепо с Turbopack в Next.js еще больше усложнял ситуацию: приходилось добавлять `turbopack.root` в `next.config.ts` и явно указывать `base` в `postcss.config.mjs`, иначе сборщик терялся в корне проекта. Интересный момент: FastAPI 0.115 получил встроенные улучшения для middleware и CORS — это было критично для взаимодействия фронтенда и бэкенда через потоковые запросы. Оказалось, многие разработчики всё ещё пытаются использовать старые схемы с простыми HTTP-ответами для голосовых данных, но streaming с SSE — это совсем другой уровень эффективности. Бэкенд отправляет куски данных по мере их готовности, фронтенд их тут же отображает, юзер не висит, дожидаясь полного ответа. Система валидации стала ключом к стабильности. На бэкенде — проверка импортов и тесты перед коммитом. На фронтенде — `npm build` перед каждым мерджем. Завел привычку писать в **ERROR_JOURNAL.md** каждую ошибку, которая повторялась: это предотвратило много дублирования проблем. В итоге получилась система, где голос идет с клиента, бэкенд его обрабатывает через FastAPI endpoints, генерирует ответ, отправляет его потоком обратно, а React UI отображает в реальном времени. Просто, но изящно. Дальше — добавление более умных агентов и интеграция с внешними API, но фундамент уже крепкий. Если Java работает — не трогай. Если не работает — тоже не трогай, станет хуже. 😄
Когда публикатор не знает, куда публиковать: миграция за 40 часов
# 40 часов миграции: спасаем социальный паблишер от самого себя Задача стояла простая, но коварная: почему заметки не публикуются в потоки? В **project-social-publisher** выписали план на 40 часов работы, и я стал разбираться в корне проблемы. Первым делом я посмотрел на архитектуру публикации. Оказалось, что система работала с заметками как с самостоятельными сущностями, не привязывая их к контексту конкретного проекта. Когда заметка попадала в API, алгоритм не знал, в какой поток её толкать, и просто зависал на шаге отправки. Это была классическая проблема: достаточно информации для создания заметки, но недостаточно для её таргетирования. Решение пришло в три этапа. Сначала я добавил поле `projectId` к заметке — теперь каждая публикация могла быть привязана к конкретному проекту. Вторая проблема была тонкая: хэштеги. Система генерировала какие-то общие #разработка, #код, но потокам нужны были специфичные для проекта метки — #bot-social-publisher, #автоматизация-контента. Пришлось переделать логику генерации хэштегов, добавив правила по типам проектов и их особенностям. Третьим этапом была доработка самого workflow публикации. В `claude_code` branch я переписал обработчик отправки в потоки: теперь перед публикацией система проверяет наличие `projectId`, валидирует хэштеги, специфичные для проекта, и только потом отправляет. Оказалось, что раньше публикация падала молча — логирование просто не было настроено. Добавил детальные логи на каждом шаге, и сразу стало видно, где система буксует. Интересный момент: когда ты работаешь с системой публикации в социальные сети, нужно помнить о rate-limiting. Каждый сервис (Telegram, Twitter, Reddit — если они в проекте) имеет свои лимиты на количество запросов в секунду. Если ты просто отправляешь заметки в цикле без очереди, система будет заблокирована в течение часа. Поэтому я внедрил простую очередь на базе setTimeout с адаптивной задержкой — система автоматически замедляется, если видит, что сервис отвечает с ошибками 429 (Too Many Requests). После 40 часов работы система наконец корректно привязывала заметки к проектам, генерировала контекстно-специфичные хэштеги и публиковала в потоки без срывов. Тесты прошли — как синтетические, так и с реальными потоками. Теперь каждая заметка приходит в нужный канал с нужными метаданными, и операторы видят, из какого проекта пришла та или иная публикация. Главный вывод: иногда проблема публикации — это не одна большая фишка, а несколько маленьких пробелов в архитектуре. Когда система не знает контекст, она не может принять правильное решение. Вот и весь секрет. *Rate limiting чинит жизнь. Но если ты забудешь про очередь — проблемы чинить нельзя.* 😄
Интерфейс, который говорит на языке оператора
# Когда интерфейс встречается с производством: как мы спасли SCADA за час планирования Проект **scada-coating** — это система управления линией электроосаждения цинка. На бумаге звучит узко и специализировано, но по факту это боевая машина, которой оператор пользуется каждый день, и каждая неправильная кнопка стоит денег. Вчера мы обнаружили, что наш интерфейс вообще не соответствует тому, как люди думают о процессе. И это была хорошей новостью — потому что мы поймали ошибку до боевого развёртывания. ## Момент истины: путаница в контексте Началось с простой, но критичной проблемы. **Оператор путал техкарты с программами выпрямителя.** Звучит как мелочь? На линии это означает: человек не понимает, применяется ли конкретная программа для цинка 10 микрометров или никеля. Техкарта — это маршрут между ванными, программа выпрямителя — это параметры электрического процесса. Они связаны, но живут в разных *ментальных моделях*. А мы упаковали их в одну вкладку, как будто они одно и то же. Когда технолог указал на это, стало ясно: нужна полная переоценка архитектуры интерфейса. Не какие-то правки, а настоящая переработка. ## Как мы разбирались в хаосе Первым делом мы разделили информацию по смыслу. Техкарты и маршруты — в первую вкладку. Программы выпрямителя с тегами (вместо просто названий) — во вторую. Теперь каждый контекст существует отдельно, и оператор видит ровно то, что ему нужно в конкретный момент. Потом дошло до вкладки *Шаги*. Там был график — красивый, интерактивный, совершенно бесполезный для редактирования. Людям нужно было кликать по линиям, чтобы менять параметры. Мы развернули логику: график теперь — справочный элемент, открывается по необходимости. Основная рабочая область — таблица, где каждый параметр шага это отдельный столбик. Консистентно со всем остальным. Техкарту мы переделали в двухуровневую структуру: основные параметры отдельно, маршрут операций отдельно. И вот интересный момент — линия может иметь несколько ванн одного назначения, которые взаимозаменяемы. Нельзя просто указать *ванна номер 3*. Нужна гибкая система выбора. Это отправило нас обратно на ревью к UX-дизайнерам, потому что редактирование должно быть не просто удобным — оно должно быть идеальным. ## Неожиданное открытие про выпрямители Технолог работает не с отдельными выпрямителями, а смотрит на них как на инструмент контроля *этапа обработки всех подвесок*. Как оператор видит линию целиком в одном месте. Мы скопировали эту логику — теперь выпрямитель показан как часть большого этапа, а не как отдельный элемент управления. ## Что важно: критический анализ вместо слепого согласия Мы не просто приняли все замечания. Каждое предложение прошло через четырёхслойный анализ: дизайнер, архитектор, технолог и разработчик смотрели на это через разные линзы. Вкладка *Линия* вообще была исключена как лишняя — технолог зайдёт под правами оператора, если ему нужна информация о состоянии линии. Результат? Не просто интерфейс. Система, которую люди на самом деле будут использовать, потому что она говорит на их языке. Почему выпрямитель плакал от радости при виде новой вкладки параметров? Потому что наконец-то его коэффициенты лежат в обычной таблице, а не размазаны по интерактивному графику! 😄
Документация врёт: что на самом деле происходит в production
# Когда документация на месте, а реальность — в другой комнате Работаю с проектом voice-agent уже несколько месяцев. Классический случай: архитектура идеально описана в CLAUDE.md, правила параллельного выполнения агентов расписаны до мелочей, даже обработка ошибок задокументирована. На бумаге всё правильно. Но потом приходит первая задача от пользователя, и выясняется: между документацией и реальностью — целая бездна. Начнём издалека. У нас есть агентская система с разделением ролей: Opus для архитектуры и bash-команд, Sonnet для имплементации, Haiku для шаблонного кода. Казалось бы, идеально. Параллельное выполнение до 4 агентов одновременно, жёсткое разделение backend'а и frontend'а. На практике же выяснилось, что в последний день активности было ноль пользовательских взаимодействий. Ноль! При 48 инсайтах от агентов. Это сигнал. Первым делом я решил проверить ERROR_JOURNAL.md — документация требует начинать с него. И тут первая проблема: файл либо не существует, либо пуст. Глобальное правило говорит: *проверь журнал ошибок перед любым диагнозом*, а его попросту нет. Это уже что-то значит. Значит, либо команда срезала углы, либо инцидентов попросту не было. Третьего не дано. Дальше я посмотрел на то, что описано в phase-плане для TMA (53 задачи во всех этапах). Документация обещает методичное разбиение работы. Проверил git log — и вот странность: некоторые коммиты с описаниями, но судя по датам, AgentCore рефакторинг якобы прошёл, но в коде я его не нашёл. Это очень типичная ситуация в больших проектах: документация отстаёт от реальности, или наоборот — расходилась на раннем этапе и никто не синхронизировал. Здесь я выучил важный урок. Когда я читал правила про управление контекстом субагентов, там чётко сказано: *не дублируй информацию, передавай минимум*. Казалось бы, конфликт с thorough-подходом. Но это не конфликт — это оптимизация. Если в документации написано, что sub-agents не выполняют Bash (автоматический deny), то параллельное выполнение задач оказывается иллюзией: все команды приходится сериализовать после файловых операций. И документация об этом ничего не говорит. **Неожиданно полезный инсайт**: читал про constraint-driven design. Оказывается, это вообще методология — начинать не с возможностей, а с ограничений. Если системе запрещены Bash-команды в параллель, нужно проектировать workflow с этим в голове с дня первого. Большинство проблем возникают потому, что документация описывает идеал, а ограничения считаются деталями. В итоге я сделал простую вещь: создал pre-flight checklist для каждого нового взаимодействия. Сначала — Read на PHASES.md, потом Git log для валидации, потом Grep для проверки реальности кода. Только *потом* я предлагаю следующие шаги. Документация классная, но реальность — источник истины. Ключевой урок: никогда не отождествляй то, что написано, с тем, что сделано. И всегда начинай с проверки, не с веры 😄
От хаоса к объектам: как переделали API для трендов
# Регистрируем API эндпоинт: как архитектура трендов выросла из хаоса документации Мне нужно было разобраться с проектом **trend-analysis** — системой для отслеживания трендов из GitHub и Hacker News. Проект жил в состоянии «почти готово», но когда я начал читать логи и документацию, выяснилось: база данных хранит обычные статьи, а нужно хранить **объекты** — сущности вроде React.js или ChatGPT, за которыми стоит десятки упоминаний. Первым делом я столкнулся с классической проблемой: эксперты предложили одну методологию определения трендов, а Глеб Куликов (архитектор системы) независимо пришёл к другой — и они совпадали на **95%**. Но Куликов заметил то, что упустили эксперты: текущая архитектура создаёт дубликаты. Одна статья о React — один тренд, вторая статья о React — второй тренд. Это как хранить 10 постов о Путине вместо одной записи о самом Путине в каталоге. Я решил реализовать **гибридную модель**: добавить слой entity extraction, чтобы система извлекала объекты из статей. Значит, нужны новые таблицы в БД (`objects`, `item_objects`, `object_signals`) и, самое важное, новые API эндпоинты для управления этими объектами. **Вот тут начинается интересная часть.** API эндпоинты я размещал в `api/auth/routes.py` — стандартное место в проекте. Но admin-endpoints для работы с объектами требовали отдельного маршрутизатора. Я создал новый файл с роутером, настроил префикс `/admin/eval`, и теперь нужно было **зарегистрировать его в main.py**. На фронтенде добавил страницу администратора для управления объектами, обновил боковую панель навигации, реализовал API-клиент на TypeScript, используя существующие паттерны из проекта. По сути, это была целая цепочка: api → typescript-client → UI components → i18n ключи. **Занимательный факт о веб-архитектуре**: корневая ошибка новичков — писать эндпоинты, не думая о регистрации роутеров. Flask и FastAPI не магическим образом находят ваши функции. Если вы создали красивый эндпоинт в отдельном файле, но забыли добавить `app.include_router()` в main.py — для клиента это будет 404 Not Found. Поэтому регистрация в точке входа приложения — это не «формальность», это **фундамент**. В итоге система сегодня: - Не ломает текущую функциональность (backward compatible) - Может извлекать объекты из потока статей - Отслеживает свойства объектов: количество упоминаний, интенсивность сентимента, иерархию категорий - Готова к полной дедупликации в Q3–Q4 Документировал всё в `KULIKOVS-METHODOLOGY-ANALYSIS.md` — отчёт на 5 фаз имплементации. Теперь архитектура стройная, и следующие разработчики не будут гадать, почему в системе 10 записей о React вместо одной. 😄 Почему Ansible расстался с разработчиком? Слишком много зависимостей в отношениях.
Логи в системном трее: как простая отладка спасла меню устройств
# Охота на баги в системном трее: как логи спасили день Проект **speech-to-text** — это приложение для распознавания речи с поддержкой выбора аудиоустройства прямо из системного трея. Казалось бы, простая задача: пользователь кликает по иконке микрофона, видит список устройств, выбирает нужное. Но реальность оказалась хитрее. ## Когда старая сборка не хочет уходить Всё началось со стандартной проблемы: после изменения кода сборка падала, потому что старый EXE-файл приложения всё ещё работал в памяти. Казалось бы, что здесь сложного — убить процесс, запустить новый. Но разработчик пошёл дальше и решил запустить приложение в **режиме разработки** прямо из Git Bash, чтобы видеть логи в реальном времени. Это сыграло ключевую роль в том, что произошло дальше. Задача была конкретной: разобраться, почему меню выбора аудиоустройства в системном трее работает странно. Пользователь кликает на "Audio Device", но что происходит дальше — неизвестно. Здесь-то и нужны были логи. ## Логирование как инструмент детектива Первое, что сделал разработчик — добавил логирование на каждый шаг создания меню устройств. Это классический подход отладки: когда ты не видишь, что происходит внутри системного трея Windows, логи становятся твоим лучшим другом. Приложение запущено в фоновом режиме. Инструкция для тестирования была простая: наведи курсор на "Audio Device" в трее, и система начнёт логировать каждый шаг процесса. Процесс загрузки моделей искусственного интеллекта занимает 10–15 секунд — это время, когда нейросетевые модели инициализируются в памяти. Кстати, это напоминает, как работают трансформеры в современных AI-системах. По сути, когда речь преобразуется в текст, система использует архитектуру на основе multi-head attention: звук кодируется в токены, каждый токен переходит в векторное представление, а затем контекстуализируется в рамках контекстного окна с другими токенами параллельно. Это позволяет системе "понять", какие части речи важны, а какие можно проигнорировать. ## Жизненный цикл одного багфикса Разработчик делал это методично: добавил логирование, перезапустил приложение с новым кодом, ждал инициализации, затем попросил выполнить действие (клик по "Audio Device"). После этого — проверка логов. Это не просто отладка. Это **итеративный цикл** обратной связи: код → перезапуск → действие → анализ логов → новое понимание. Каждая итерация приносила всё больше информации о том, как именно система ведёт себя на уровне системного трея. Главный вывод: когда ты работаешь с компонентами операционной системы (вроде системного трея Windows), логирование становится не просто удобством, а необходимостью. Без логов ты работаешь вслепую. ## Что дальше На этот момент приложение работало, логирование было активно, и любое действие пользователя оставляло след в логах. Это была база для настоящей отладки — уже известно, как и где начать искать проблему. Разработчик научился важному уроку: **никогда не недооценивай силу логирования при работе с системными компонентами**. Это, конечно, не панацея, но когда ты охотишься на баги в чёрном ящике операционной системы, логи — это твой фонарик. Если NumPy работает — не трогай. Если не работает — тоже не трогай, станет хуже. 😄
Voice-Agent: как монорепо не рухнул под собственным весом
# Как Claude Code спас voice-agent от архитектурного хаоса Проект **voice-agent** оказался передо мной как незаконченный пазл: монорепозиторий с Python-бэкендом для обработки аудио и Next.js-фронтендом для интерфейса. Разработчик уже наметил архитектуру в документах, но требовалось реализовать суть проекта — связать асинхронную обработку речи, WebSocket-коммуникацию и сложную логику распознавания в один работающий механизм. Первая сложность: необходимо было писать и отлаживать код одновременно на трёх языках, не запутавшись в структуре монорепозитория. Задача началась с картографирования. Вместо привычного «давайте быстренько добавим функцию» я потратил время на изучение документации в `docs/tma/` — там лежали все архитектурные решения, объясняющие, почему выбраны именно эти подходы. Эта работа оказалась ключевой: знание причин проектных решений спасло меня от десятков потенциальных ошибок позже. Первая реальная задача была про потоковую обработку аудио в реальном времени. Стоял выбор: использовать простой опрос сокетов или event-driven архитектуру? Решение пришло с использованием асинхронных генераторов Python вместе с aiohttp для non-blocking операций. Звучит абстрактно, но практически это означало, что сервер теперь мог одновременно обрабатывать сотни клиентов без блокировки основного потока. Неожиданный момент случился при рефакторинге обработки текста. Обнаружилось, что синхронная функция создавала скрытую очередь запросов и вызывала каскадные задержки. Переписал на асинхронность — и задержка упала с 200 ms до 50 ms одним движением. Это был классический случай, когда архитектурное решение имеет экспоненциальный эффект на производительность. Вот важный момент, который я бы посоветовал каждому, работающему с Next.js в монорепозитории: Turbopack (встроенный bundler) может некорректно определить корневую директорию проекта и начать искать зависимости не в папке приложения, а в корне репозитория. Это вызывает каскадные ошибки с импортами. Решение банально просто, но его узнают либо опытом, либо от коллеги: нужно явно указать `turbopack.root` в `next.config.ts` и настроить базовый путь в `postcss.config.mjs`. Это элементарно, когда знаешь. За пару сессий разработчик перешёл от «давайте напишем фичу» к «давайте выберем правильный инструмент для каждой задачи». aiosqlite для асинхронного доступа к данным, WebSocket для real-time коммуникации, TypeScript для типобезопасности фронтенда — каждое решение теперь имеет обоснование. Voice-agent получил солидный фундамент, и главное открытие: хороший AI-ассистент — это не замена опыту, а его турбо. Честно? Это как работать с очень внимательным senior-разработчиком, который помнит все паттерны и никогда не пропустит edge case 😄
Привязал бота к Strapi: потоки, синхронизация и локальный маппер
# Как я связал бота и Strapi: история о потоках, тестах и синхронизации Задача стояла такая: bot-social-publisher — мой проект, который вытягивает заметки о разработке и публикует их на сайт borisovai.tech. Раньше каждая заметка была независимой статьёй в Strapi. Но это было скучно. Хотелось превратить разбросанные публикации в **потоки** — контейнеры, где все заметки одного проекта живут вместе, с общим описанием, категориями и тегами. Типа: "Поток разработки my-project: 5 заметок, последние фичи и баг-фиксы". Первым делом открыл backend на Node.js + Strapi. Там уже была база под API, но нужно было достроить. Добавил параметр `thread_external_id` в функцию `publishNote()` — теперь заметка знает, к какому потоку её привязать. Создал новый маршрут `PUT /api/v1/threads/:id` для обновления описания и тегов потока. И ещё важный момент: поток может быть **пустым контейнером** без заметок внутри. Это позволяет создать структуру заранее, прежде чем летят первые публикации. Потом переключился на Python-часть бота. Вот тут уже кипела работа. Добавил таблицу `thread_sync` в SQLite — маппер, который запоминает: "проект X → поток с ID Y". Зачем это нужно? Когда бот публикует вторую заметку по тому же проекту, ему нужно мгновенно знать, какой поток уже создан. Без этого маппинга пришлось бы каждый раз ходить на API и искать нужный поток — медленно и ненадёжно. Создал отдельный модуль **ThreadSync** с методом `ensure_thread()`. Он проверяет локальную БД, и если потока нет — создаёт его через API, кеширует результат. После успешной публикации запускается `update_thread_digest()` — она берёт данные из базы ("3 фичи, 2 баг-фикса"), форматирует на русском и английском и пушит обновление потока обратно в Strapi через PUT. Вся логика живёт теперь в **WebsitePublisher** — инициализирует ThreadSync, вызывает его перед и после публикации. Асинхронно, с `aiosqlite` для неблокирующего доступа к базе. Неожиданно выяснилось вот что: обычно Strapi используют как просто контейнер для контента. А здесь я его заставляю выполнять структурирующую роль. Потоки — это не папочки в интерфейсе, это полноценные сущности API с собственной логикой обновления. Потребовалось хорошее понимание Strapi: разница между `POST` (создание) и `PUT` (обновление), роль `external_id` для связи внешних систем, обработка локализации (ru/en в одном вызове). Закоммитил изменения — Git наругался на CRLF vs Unix, но я коммитнул только три реально изменённых файла: database.py, thread_sync.py, website.py. Результат: 70 тестов проходят, 1 скипнут. Система работает. Теперь когда бот публикует заметку, происходит магия: проверяется локальная БД → если потока нет, создаётся → заметка летит в Strapi с привязкой → тут же обновляется описание потока. Всё это видно на https://borisovai.tech/ru/threads. Зелёные тесты — лучший знак того, что большая архитектурная работа прошла чисто. 😄
Волшебный токен GitLab: от поиска до первого скопирования
# Как я чуть не сломал CI/CD, ища волшебный токен В проекте **borisovai-admin** встала задача: нужно проверять статус GitLab pipeline прямо из CI/CD, чтобы убедиться, что деплой прошёл успешно. Звучит просто, но для автоматизации требуется *Personal Access Token* — штука более секретная, чем пароль, потому что даёт доступ к API. Первым делом я попытался вспомнить, где в GitLab хранятся эти токены. Инстинкт подсказал: где-то в настройках профиля. Но вот незадача — интерфейс GitLab меняется, документация отстаёт от реальности, и каждый третий форум советует что-то своё. Начал искать по URL-адресам, как детектив, собирающий пазл. Выяснилось, что нужно открыть ровно вот этот URL: `https://gitlab.dev.borisovai.ru/-/user_settings/personal_access_tokens`. Не Settings, не API, не Profile — именно этот путь. Туда я и попал, нажал на **Add new token**, и тут начались интересные подвопросы. **Правило первое:** токену нужно дать имя, которое потом разберёшься. Назвал его `Claude Pipeline Check` — так хотя бы будет понятно, зачем он при аудите. **Правило второе:** scope. Здесь я едва не дал полный доступ, но потом вспомнил, что токену нужно только чтение API — `read_api`. Ни write, ни delete. Безопасность прежде всего. После создания токен показывается ровно один раз. Это не шутка. Потом он скрывается в звёздочках, и если забыл скопировать — удаляй и создавай заново. Я это, конечно, проверил на практике 😅 Интересный момент: GitLab разделяет токены по scopes, как OAuth, но работают они как обычные API-ключи. Каждый токен привязан к аккаунту пользователя и срабатывает для всех их проектов. Это значит, что если кто-то скомпрометирует токен, он сможет читать всё, за что этот пользователь имеет права. Поэтому в боевых системах их хранят в **secret** переменных CI/CD, а не в коде. **Что дальше?** После получения токена я мог бы проверить pipeline двумя способами: либо через браузер по ссылке `https://gitlab.dev.borisovai.ru/tools/setup-server-template/-/pipelines`, либо запросить API через curl с заголовком авторизации. Для **borisovai-admin** выбрали первый вариант — простой и понятный. Урок, который я взял: в современной разработке половина сложностей прячется не в коде, а в конфигурации доступа. И всегда стоит проверить документацию именно для вашей версии сервиса — то, что работало год назад, может просто уехать в другой URL. --- Что сказал GitLab, когда разработчик забыл скопировать токен? «Вот тебе урок — я показываю его только один раз!» 😄
Три коммита против хаоса: как я спасал расчёты скоринга
# Когда баги в расчётах больнее, чем я думал: история исправления системы скоринга Вот такой момент: сидишь ты, смотришь на результаты работы своей системы анализа трендов в проекте **trend-analysis**, и понимаешь — что-то не так с оценками. Пользователи видят неправильные значения, frontend показывает одно, backend считает совсем другое, и где-то в этом хаосе теряются ваши данные о трендах. ## Началось с простого: поиск несоответствия Задача была такой: **унифицировать систему скоринга между страницей трендов и страницей анализа**, плюс сделать её консистентной на всех слоях приложения. Проблема скрывалась в деталях. На бэкенде я обнаружил, что поле для зоны влияния называлось `strength`, но фронтенд ожидал `impact`. Вроде мелочь, но эта мелочь ломала весь расчёт оценки — данные просто не доходили до формулы. Первым делом создал feature-ветку `fix/score-calculation-and-display`, чтобы иметь безопасное место для экспериментов. Это правило номер один: никогда не чини критичное на main. ## Три коммита — три фикса **Коммит первый** (`6800636`) — объединил layouts страниц тренда и анализа. Оказалось, что кнопки были разбросаны в разных местах, компонента Sparkline находилась не там, где нужно, и показатель уверенности (confidence) был спрятан в глубины интерфейса. Переделал разметку, привёл всё к общему знаменателю. **Коммит второй** (`08ed4cd`) — вот тут засада. Бэкенд API 0.3.0 использовал название поля `impact`, а я в калькуляторе оценки искал `strength`. Результат: null вместо числа. Исправил — и вдруг всё заработало. Казалось бы, переименование в одном месте, но оно спасло половину функциональности. **Коммит третий** (`12cc666`) — уже фронтенд. Функция `formatScore` нормализовала значения неправильно, и `getScoreColor` работал с неправильной шкалой. Переделал под шкалу 0–10, убрал лишнюю нормализацию — сейчас скор отображается именно так, как его считает бэкенд. ## Почему это вообще произошло? Типичная история: когда несколько разработчиков работают над одной системой в разное время, контракт между бэкендом и фронтендом расходится. Никто не виноват — просто один переименовал поле, другой не знал об этом. Было бы хорошо иметь автоматические тесты контрактов (contract testing), которые бы сразу эту несогласованность выловили. GraphQL был бы удобнее — типизация спасла бы много мук. ## Что дальше? Merge Request готов к созданию. API сервер уже перезапущен и слушает на `http://127.0.0.1:8000`. Vite dev server для фронтенда работает в фоне с HMR, поэтому изменения подхватываются моментально. Остаётся дождаться review коллег, смёрджить в main и развернуть. Урок на будущее: синхронизируй контракты между слоями приложения через документацию или, ещё лучше, через code generation из единого источника истины. 😄 **GitLab MR** — как свидание вслепую: никогда не знаешь, найдут ли рецензенты ошибки, которые не заметил ты.
Давай сделаем потоки разработки.
# Давайте сделаем потоки разработки: от идеи к системе сбора трендов Проект **bot-social-publisher** рос, и вот встала новая задача: нужно организовать рабочие процессы так, чтобы каждый проект был отдельным потоком, а заметки собирались по этим потокам. Звучит просто, но это требовало архитектурного решения. Я полез в документацию на сайте (https://borisovai.tech/ru/threads) и понял: нужна полноценная система управления потоками разработки с минидайджестом в каждом потоке и обновлением потока при публикации заметки. Одновременно с этим приходилось разбираться с тем, что творилось в подпроекте **trend-analysis**. Система анализирует тренды с Hacker News и выставляет им оценки влияния по шкале от 0 до 10. Казалось бы, простая арифметика, но два анализа одного и того же тренда выдавали разные score — 7.0 и 7.6. Вот это нужно было развязать срочно. Первым делом я погрузился в исходный код. В `api/routes.py` нашёл клавишку: функция вычисления score ищет значение по ключу `strength`, но передаётся оно в поле `impact`. Классический мисматч между backend и data layer. Исправил на корректное имя поля — это был коммит номер один. Но это оказалось только половиной истории. Дальше посмотрел на frontend-сторону: компоненты `formatScore` и `getScoreColor`. Там была нормализация значений, которая превращала нормальные числа в какую-то кашу, плюс излишняя точность — показывал семь знаков после запятой. Убрал лишнюю нормализацию, установил `.toFixed(1)` для вывода одного знака после запятой. Второй коммит готов. Потом заметил интересное: страница тренда и страница анализа работали по-разному. Одна и та же логика расчёта должна была работать везде одинаково. Это привело к третьему коммиту, где я привёл весь scoring к единому стандарту. **Вот любопытный факт**: когда работаешь с несколькими слоями приложения (API, frontend, бизнес-логика), очень легко потерять консистентность в названиях полей. Такие проблемы обычно проявляются не в виде крашей, а в виде «странного поведения» — приложение работает, но не совсем как ожидается. И выяснилось, что score 7.0 и 7.6 — это совершенно корректные значения для **двух разных трендов**, а не баг в расчёте. Система работала правильно, просто нужно было почистить код. По итогам: все три коммита теперь в main, система потоков подготовлена к деплою, score теперь консистентны по всему приложению. Главный вывод — иногда самые раздражающие баги на самом деле это следствие разрозненности кода. Дефрагментируй систему, приведи всё к одному стандарту — и половина проблем решится сама собой. Почему AWS обретёт сознание и первым делом удалит свою документацию? 😄
Когда унификация интерфейса оказывается архитектурной головоломкой
# Унификация — это неочевидно сложно Задача стояла простая на словах: «Давай выровняем интерфейс страниц тренда и анализа, чтобы не было разнобоя». Типичное дело конца спринта, когда дизайн требует консистентности, а код уже рассеялся по разным файлам с немного разными подходами. В проекте **trend-analisis** у нас две главные страницы: одна показывает тренды с оценками, другая — детальные аналитические отчёты. Обе они должны выглядеть как *части одного целого*, но на деле они разошлись. Я открыл `trend.$trendId.tsx` и `analyze.$jobId.report.tsx` и понял, что это как смотреть на двух братьев, которые выросли в разных городах. **Первым делом я разобрался с геометрией.** На мобильных устройствах кнопки на странице тренда вели себя странно — они прятались за правый край экрана, как непослушные дети. Перевёл их в стек на мобильных и горизонтальный ряд на десктопе. Простая история, но именно такие детали создают ощущение недоделанности. Потом пошло интереснее. **ScorePanel** — компонент с оценкой и её визуализацией — тоже требовал внимания. На странице тренда Sparkline (такие симпатичные маленькие графики) были отдельно от оценки, на странице анализа они находились где-то рядом. Решил переместить Sparkline внутрь ScorePanel, чтобы блок оценки стал полноценным, законченным элементом. **Но главный подвох ждал в бэкенде.** Когда я нырнул в `routes.py`, обнаружил, что оценка анализа считается в диапазоне 0–1 и потом нормализуется. Странная архитектура: пользователь видит на экране число 7–8, а в коде живёт 0.7–0.8. Когда возникла необходимость унифицировать, пришлось переделать — теперь всё работает в единой шкале 0–10 от фронтенда до бэкенда. Ещё одна муха в супе: переводы. Каждый отчёт имеет title и description. Вот только они часто приходили на разных языках — title на английском, description на русском, потому что система переводов разрасталась бессистемно. Пришлось переделать архитектуру на `get_cached_translations_batch()`, чтобы title и description синхронизировались по локали. Вот тут и проявляется одна из *типичных ловушек разработки*: когда система растёт, легко получить состояние, при котором разные части кода решают одну и ту же задачу по-разному. Кэширование переводов, кэширование данных, нормализация чисел — каждая из этих проблем порождает своё микрорешение, и вскоре у вас сложная паутина зависимостей. Решение: честный код-ревью и документирование паттернов, чтобы новичок не добавил пятый способ кэширования. **В итоге:** две страницы теперь выглядят как надо, API вернулся к нормальным оценкам (7–8 вместо 1), переводы синхронизированы. Git commit отправлен, бэкенд запущен на порту 8000. Дальше в плане новые исправления — благо материал есть. Чему научился: унификация — не просто про UI, это про согласованность логики по всему стеку. Порой проще переделать целый компонент, чем мучиться с костылями. 😄 Почему backend разработчик плюёт на фронтенд? Потому что он работает в консоли и ему всё равно, как это выглядит.
Когда GitLab Runner нашел 5 ошибок TypeScript за 9 секунд
# GitLab Runner сломал сборку: как мы спасали TypeScript проект Понедельник, 10 февраля. В 17:32 на сервере **vmi3037455** запустился очередной CI/CD пайплайн нашего проекта **trend-analisis**. GitLab Runner 18.8.0 уверенно начал свою работу: клонировал репозиторий, переключился на коммит f7646397 в ветке main, установил зависимости. Всё шло как надо, пока... Сначала казалось, что всё в порядке. `npm ci` отработал чисто: 500 пакетов установилось за 9 секунд, уязвимостей не найдено. Команда `npm run build -- --mode production` запустилась, TypeScript компилятор включился. И вот тут — **взрыв**. Пять ошибок TypeScript сломали всю сборку. Сначала я подумал, что это очередное невезение с типизацией React компонентов. Но посмотрев внимательнее на стек ошибок, понял: это не просто синтаксические проблемы. Это был признак того, что в коде **фронтенда рассинхронизировались типы** между компонентом и API. Проблема первая: в файле `src/routes/_dashboard/analyze.$jobId.report.tsx` компонент ожидал свойства **trend_description** и **trend_sources** на объекте AnalysisReport, но они попросту не существовали в типе. Это классический случай, когда один разработчик обновил API контракт, а другой забыл синхронизировать тип на фронтенде. Проблема вторая: импорт `@/hooks/use-latest-analysis` исчез из проекта. Компонент `src/routes/_dashboard/trend.$trendId.tsx` отчаянно его искал, но находил только воздух. Кто-то либо удалил хук, либо переместил его, не обновив импорты. Проблема третья совсем коварная: в роутере используется типизированная навигация (похоже, TanStack Router), и при переходе на страницу `/analyze/$jobId/report` не хватало параметра **search** в типе. Компилятор был совершенно прав — мы пытались пройти валидацию типов с неполными данными. Иронично, что всё это выглядит как обычная рабочая пятница в любом JavaScript проекте. TypeScript здесь одновременно наш спаситель и палач: он не позволит нам развернуть баг в production, но заставляет потратить время на то, чтобы привести типы в порядок. **Интересный факт:** GitLab Runner использует **shallow clone** с глубиной 20 коммитов для экономии трафика — видите параметр `git depth set to 20`. Это означает, что пайплайн работает быстро, но иногда может не найти необходимые коммиты при работе с историей. В данном случае это не помешало, но стоит помнить при отладке. В итоге перед нами встала классическая задача: синхронизировать типы TypeScript, переимпортировать удалённые хуки и обновить навигацию роутера. Сборка не пройдёт, пока всё это не будет в порядке. Это момент, когда TypeScript раскрывает свою суть: быть стеной между плохим кодом и production. Дальше предстояла работа по восстановлению целостности типов и проверка, не сломали ли мы что-нибудь ещё в спешке. Welcome to the JavaScript jungle! 😄
Монорепо как зеркало: когда Python и JS живут в одном доме
# Монорепо как зеркало: Python + Next.js в одном проекте **Завязка** Представьте ситуацию: вы разработчик и в ваших руках проект *voice-agent* — голосовой помощник на основе Claude, построенный как монорепо. С одной стороны Python-backend (FastAPI, aiogram для Telegram), с другой — Next.js фронтенд (React 19, TypeScript 5.7) для Telegram Mini App. Звучит здорово, но вот в чём подвох: когда в одном репозитории живут две экосистемы с разными правилами игры, управлять ими становится искусством. **Развитие** Первой проблемой, которая выпрыгнула из неоткуда, была **забывчивость переменных окружения**. В Python проект использует `pydantic-settings` для конфигурации, но выяснилось, что эта библиотека не экспортирует значения автоматически в `os.environ`. Результат? Модульный код, читавший переменные прямо из окружения, падал с загадочными ошибками. Пришлось документировать эту ловушку в ERROR_JOURNAL.md — живом архиве подводных камней проекта, где уже скопилось десять таких «моментов истины». Далее встал вопрос архитектуры. Backend требовал **координатора** — центрального паттерна, который бы оркестрировал взаимодействие между агентами и фронтенд-запросами. На бумаге это выглядело идеально, но в коде его не было. Это создавало технический долг, который блокировал Phase 2 разработки. Пришлось вводить **phase-gate валидацию** — автоматическую проверку, которая гарантирует, что прежде чем переходить к следующей фазе, все артефакты предыдущей действительно на месте. В процессе появилась и проблема с **миграциями базы данных**. SQLite с WAL-режимом требовал аккуратности: после того как junior-агент создавал файл миграции, она не всегда применялась к самой БД. Пришлось вводить обязательный чек: запуск `migrate.py`, проверка таблиц через прямой SQL-запрос, документирование статуса. Без этого можно часами отлавливать фантомные ошибки импорта. **Познавательный блок** Интересный факт: монорепо — это не просто удобство, это *культурный артефакт* команды разработки. Google использует одно гигантское хранилище для всего кода (более миллиарда строк!), потому что это упрощает синхронизацию и рефакторинг. Но цена высока: нужны инструменты (Bazel), дисциплина и чёткие протоколы. Для нашего voice-agent это значит: не просто писать код, а писать его так, чтобы Python-part и Next.js-part *доверяли друг другу*. **Итог** В итоге сложилась простая истина: монорепо работает только если есть **система проверок**. ERROR_JOURNAL.md превратился не просто в логирование ошибок, а в живой артефакт культуры команды. Phase-gate валидация стала гарантией, что при параллельной работе нескольких агентов архитектура не съезжает в стороны. А обязательная проверка миграций — это не занудство, а спасение от трёх часов ночного отлавливания, почему таблица не там, где ей быть. Главный урок: в монорепо важна не столько архитектура, сколько **честность системы**. Чем раньше вы перейдёте от надежды на память к автоматическим проверкам, тем спокойнее спать будете. Почему Python и Java не могут дружить? У них разные dependency trees 😄
Umami Analytics: как я сделал админ-панель data-driven
# Самостоятельная аналитика: как я превратил borisovai-admin в data-driven продукт Несколько месяцев назад передо мной встала типичная для любого владельца проекта проблема: я совершенно не видел, кто и как использует мою админ-панель **borisovai-admin**. Google Analytics казался избыточным (и страшным с точки зрения приватности), а простой счётчик посещений — примитивным. Нужно было что-то лёгкое, приватное и полностью под своим контролем. Выбор пал на **Umami Analytics** — открытую веб-аналитику, которая уважает приватность пользователей, не использует cookies и полностью GDPR-compliant. Главное же — её можно развернуть самостоятельно, прямо в своей инфраструктуре. ## Четыре этапа внедрения **Первый шаг — упростить развёртывание.** Стандартная Umami требует двух контейнеров (приложение + PostgreSQL), но для небольшого проекта это избыточно. Я нашёл fork **maxime-j/umami-sqlite**, который использует SQLite — файловую БД в одном контейнере. Экономия памяти была существенной: вместо ~300 MB получил ~100 MB. Затем написал скрипт **install-umami.sh** из семи шагов, который может быть запущен много раз без побочных эффектов (идемпотентный — именно это было важно для автоматизации). **Второй этап — автоматизировать через CI/CD.** Создал два job'а в пайплайне: один автоматически ставит Docker (если его нет), второй — развёртывает саму Umami. Добавил health check, чтобы пайплайн не переходил к следующему шагу, пока контейнер не будет готов. Инкрементальный деплой через **deploy-umami.sh** позволяет обновлять конфигурацию без перезагрузки приложения. **Третий этап — дать пользователям интерфейс.** Создал страницу **analytics.html**, где каждый новый сервис может получить код для интеграции отслеживания. Плюс добавил API endpoint `GET /api/analytics/status` для проверки, всё ли работает. Async-скрипт Umami весит всего ~2 KB и не блокирует рендеринг страницы — вот это я ценю. **Четвёртый этап — документировать.** Написал **AGENT_ANALYTICS.md** с инструкциями для будущих разработчиков, обновил главный **CLAUDE.md** таблицей всех сервисов. ## Что интересного я узнал Оказывается, боль большинства разработчиков с традиционной аналитикой — это не функциональность, а приватность. Umami решает это элегантно: скрипт отправляет только агрегированные данные (сессии, страницы, источники трафика) без ID пользователей и истории кликов. А главное — нет необходимости в **consent banner**, который все равно раздражает пользователей. Порт **3001** внутри контейнера пробросил через **Traefik** на HTTPS-домены `analytics.borisovai.ru` и `analytics.borisovai.tech`. Вообще, это я оценил: такая простота развёртывания чуть ли не впервые в моём опыте с self-hosted решениями. Встроенная авторизация в самой Umami (не потребовался дополнительный Authelia) — и это экономия на инфраструктуре. Один лайфхак: чтобы скрипт аналитики не блокировался AdBlock, назвал его `stats` вместо стандартного `umami` — простой способ обойти базовые фильтры. ## Итог Теперь **borisovai-admin** наконец-то видит себя со стороны. Я получил данные о том, какие страницы реально используют люди, откуда они приходят и сколько времени длятся сессии. Всё это — на своём сервере, без третьих лиц и без чувства вины перед пользователями. Следующий шаг — подключить аналитику ко всем остальным сервисам проекта. Это уже не задача месяца, а скорее вопрос пары часов на каждый сервис. Учимся: иногда лучший инструмент — это не самый популярный, а самый честный. 😄
Unit-тесты зелёные, а бот не работает: гонка условий в Telegram
# Когда unit-тесты лгут: как я запустил систему доступа в реальном Telegram **bot-social-publisher** выглядел как отличный проект для спринта. Полнофункциональный Telegram-бот с командами, памятью, интеграциями. Я предложил добавить управление доступом — чтобы владельцы чатов могли приватизировать разговоры с ботом. Звучит просто: только авторизованные пользователи видят ответы. Идеально для персональных AI-ассистентов или закрытых групп модерации. Я развернул **ChatManager** — класс с методом `is_allowed()`, который проверяет разрешение пользователю писать в конкретный чат. Добавил миграцию SQLite для таблицы `managed_chats`, обвязал всё middleware'ами в **aiogram**, написал четыре команды: `/manage add`, `/manage remove`, `/manage status`, `/manage list`. Unit-тесты прошли с ликованием. **pytest** выдал зелёный статус. Документация? Позже, мол. Потом наступил момент истины. Запустил бота локально через `python telegram_main.py`. В личном чате отправил `/manage add` — чат добавился, режим приватности активировался. Отправил обычное сообщение — ответ пришёл. Открыл второй аккаунт, отправил то же — бот молчит. Отлично, система контроля работает! Но не совсем. **Первая проблема** вскрылась при быстрых командах подряд. **aiogram** работает асинхронно, как и **aiosqlite**. Получилась коварная гонка условий: middleware проверяет разрешения раньше, чем транзакция в БД успела закоммититься. Бот получает `/manage add`, начинает писать в таблицу, но собственная система контроля выполняет проверку за доли секунды до того, как данные туда попадут. На unit-тестах такое не видно — там всё выполняется последовательно. **Вторая проблема** — SQLite и одновременные асинхронные обработчики. Один handler записывает изменение в БД, другой в это время проверяет состояние и видит старые данные, потому что `commit()` ещё не произошёл. Мне помогли явные транзакции и аккуратная расстановка `await`'ов — гарантия того, что каждая операция завершится перед следующей. Вот в чём разница между unit-тестами и интеграционными испытаниями: первые проверяют, что функция работает в идеальных условиях. Вторые отправляют реальное сообщение через серверы Telegram, пускают его через весь стек middleware, обрабатывают в handler'е, записывают в БД и возвращают результат. Тесты говорили: всё работает. Реальность показала: медленно и с условиями. После боевых испытаний я заполнил полный чеклист: импорты класса, валидация миграции, проверка всех команд в живом Telegram, полный набор pytest, документация в `docs/CHAT_MANAGEMENT.md` с примерами архитектуры. Восемь пунктов — восемь потенциальных взрывов, которые благополучно не произошли. Урок выучен: асинхронность и базы данных требуют больше, чем зелёные unit-тесты. Реальный Telegram и реальная асинхронность покажут то, что никогда не отловишь в тестовом окружении. 😄 Говорят, асинхронные баги в облаке GCP просто растворяются — поэтому их никто не находит.
Когда unit-тесты лгут: боевые испытания Telegram-бота
# Telegram-бот на боевых испытаниях: когда unit-тесты не подстраховывают Проект **bot-social-publisher** начинался просто. Полнофункциональный Telegram-бот с памятью, командами, интеграциями. Но вот на очередную спринт-планерку я заявил: добавим систему управления доступом. Идея казалась пустяковой — дать владельцам возможность приватизировать свои чаты, чтобы только они могли с ботом общаться. Типичный use case: персональный AI-ассистент или модератор в закрытой группе. Теория была прекрасна. Я развернул **ChatManager** — специальный класс с методом `is_allowed()`, который проверяет, разрешена ли пользователю отправка сообщений в конкретный чат. Добавил миграцию SQLite для таблицы `managed_chats`, прошил middleware в **aiogram**, написал обработчики команд `/manage add`, `/manage remove`, `/manage status`, `/manage list`. Unit-тесты прошли с зелёным светом — `pytest` даже не чихнул. Документация пока отложена, но это же детали! Потом наступил момент истины. Запустил бота локально через `python telegram_main.py`, переключился в личный чат и отправил первую `/manage add`. Бот записал ID чата, переключился в режим приватности. Нормально! Попробовал отправить обычное сообщение — ответ пришёл. Открыл чат со своего второго аккаунта, отправил то же самое — тишина. Бот ничего не ответил. Перфект, middleware работает. Но не всё было так гладко. Первая проблема вылезла при быстрых командах подряд. В асинхронной архитектуре **aiogram** и **aiosqlite** есть коварная особенность: middleware может проверить разрешения раньше, чем транзакция успела закоммититься. Получилась гонка условий — бот получал `/manage add`, начинал записывать в БД, но его собственная система контроля доступа успевала выполнить проверку за доли секунды до того, как данные попали в таблицу. Казалось бы, логические ошибки не могут быть незаметны в коде, но тут они проявились только в полевых условиях. Вторая проблема — SQLite при одновременной работе нескольких асинхронных обработчиков. Один handler записывал изменение в БД, а другой в это время проверял состояние — и видел старые данные, потому что `commit()` ещё не произошёл. Гарантировать консистентность мне помогли явные транзакции и аккуратная работа с await'ами. Вот в чём прелесть интеграционного тестирования: ты отправляешь реальное сообщение через Telegram-серверы, оно проходит через webhook, пробегает весь стек middleware, обрабатывается обработчиком, записывается в БД и возвращается пользователю. Unit-тесты проверяют логику функции. Интеграционные тесты проверяют, работает ли всё это вместе в реальности. И оказалось, что между «работает в тесте» и «работает в реальности» огромная разница. После всех боевых испытаний я заполнил чеклист: проверка импортов класса, валидация миграции, тестирование всех команд в Telegram, запуск полного набора pytest, документирование в `docs/CHAT_MANAGEMENT.md` с примерами и описанием архитектуры. Восемь пунктов — восемь потенциальных точек отказа, которые благополучно миновали. Урок на будущее: когда работаешь с асинхронностью и базами данных, unit-тесты — это необходимо, но недостаточно. Реальный Telegram, реальные пользователи, реальная асинхронность покажут то, что никогда не отловить в тестовом окружении. 😄 Иногда мне кажется, что в облаке **GCP** ошибка при доступе просто уходит в облака, так что никто её не найдёт.
Когда unit-тесты зелёные, а бот падает в продакшене
# Проверяем Telegram-бота в боевых условиях: когда unit-тесты врут Любой разработчик знает эту ситуацию: твой код прошёл все тесты в PyTest, green lights светят, CI/CD улыбается. Но стоит запустить приложение в реальной среде — и вдруг выскакивают проблемы, которые перестанут выглядеть как волшебство, как только ты их найдёшь. Со мной произошла именно эта история на проекте **bot-social-publisher**, когда я добавил в Telegram-бота систему управления доступом. ## Задача казалась элементарной Надо было реализовать для бота фишку с приватными чатами. Идея простая: если владелец чата напишет `/manage add`, бот переходит в режим приватности и начинает отвечать только ему. Команда `/manage remove` открывает доступ всем обратно. Плюс туда же добавил `/recall` и `/remember` для сохранения истории разговоров. На бумаге всё выглядело как три строки кода в middleware'е, которые проверяют ID пользователя перед обработкой сообщения. Я написал unit-тесты, всё прошло. Но реальный Telegram — совсем другой зверь. ## Боевые испытания в реальной среде Первым делом поднял бота локально через `python telegram_main.py` и начал его "пилить" из реального Telegram аккаунта. Написал `/manage add` — бот записал ID чата в таблицу `managed_chats` в SQLite и переключился в режим приватности. Проверил middleware `permission_check.py` — всё срабатывает корректно, обработка заблокирована для чужих. Хорошо. Потом попросил друга написать то же самое сообщение со своего аккаунта. Ожидал — ничего не случится. И действительно, бот промолчал. Отлично, система работает как надо. Финальный тест: я написал `/manage remove`, друг снова отправил сообщение — и бот ответил. Приватность отключена, доступ восстановлен. Казалось бы, победа. Но потом обнаружилась подвох. ## Гонка условий в асинхронном коде Оказалось, что в асинхронной архитектуре **aiogram** есть коварная особенность: middleware проверяет доступ, а запись в БД может ещё не завершиться. Получилась гонка условий — команда `/manage add` срабатывала, но контроль доступа успевал проверить разрешения *до* того, как данные попали в таблицу. Пришлось оборачивать insert'ы в explicit `await`, чтобы гарантировать консистентность. Другая проблема с SQLite: при одновременной работе нескольких асинхронных обработчиков изменения одного из них могут быть не видны другим, пока не произойдёт `commit()`. Контроллер доступа проверял одно, а в реальности БД содержала совсем другое. Решение было банальным — явные транзакции, но выяснить это можно было только через реальное тестирование. ## Познавательный момент об асинхронности Здесь скрывается типичная ловушка разработчиков, переходящих с синхронного кода на async/await: асинхронный код **кажется** последовательным в написании, но на самом деле может выполняться в самых неожиданных порядках. Когда ты пишешь `await db.execute()`, это не значит, что все предыдущие операции уже завершены в других корутинах. Нужна явная синхронизация через контекстные менеджеры или явные commit'ы. ## Итог: документируем опыт После всех интеграционных тестов я задокументировал находки в `docs/CHAT_MANAGEMENT.md`, добавил примеры использования в README.md и описал полную архитектуру ChatManager'а. Теперь система готова к работе с приватными чатами и конфиденциальными данными. Главный урок: unit-тесты проверяют логику в вакууме, но реальный мир полон асинхронности, сетевых задержек и race conditions. Никакой PyTest не найдёт то, что видно только в продакшене. Поэтому перед тем, как праздновать зелёный CI/CD, всегда имеет смысл руки испачкать в реальной среде. 😄 Что говорит разработчик после запуска асинхронного кода? «У меня было семь ошибок, теперь их четырнадцать, но они более интересные».