Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
Когда голосовой агент забывает встречи: учим AI слушать, не переспрашивая
# Когда AI забывает контекст: как мы учим голосовых агентов помнить о встречах Павел получил от своего AI-ассистента странный ответ. Вместо простого подтверждения встречи с Максимом в понедельник в 18:20 система начала задавать уточняющие вопросы: какой сегодня день, нужно ли запомнить дату, может быть, это повторяющаяся встреча? Звучит как помощник с амнезией, верно? Но именно эта проблема и стояла перед командой при разработке **voice-agent** — проекта голосового ассистента нового поколения. **Завязка:** проект работает на стыке нескольких сложных технологий. Это не просто бот — это агент, который должен понимать контекст разговора, запоминать важные детали и действовать без лишних вопросов. Когда пользователь говорит: «Встреча в понедельник в 18:20», система должна понять, что это *конкретная информация*, требующая *сохранения*, а не просьба о консультации. Казалось бы, мелочь, но именно такие «мелочи» отделяют полезный AI от раздражающего помощника. **Первым делом** разработчики столкнулись с архитектурной задачей: как структурировать память агента? Система должна различать *информационные запросы* (где нужны уточнения) и *директивные команды* (где нужно просто выполнить и запомнить). Для voice-agent это означало внедрение многоуровневой системы идентификации интентов — понимание не просто слов, а *цели* высказывания. Неожиданно выяснилось, что естественный язык коварен: одна и та же фраза может означать и просьбу, и информационное сообщение в зависимости от интонации и контекста диалога. Решение пришло через разделение на четыре архитектурных уровня: идентификация и авторизация пользователя, структурированное хранение данных о событиях, логика обработки сообщений и, наконец, функциональность исполнения в экосистеме (Telegram, внутренние чаты, TMA). Каждый уровень отвечает за свой кусок пазла. Система теперь не просто парсит текст — она *понимает ролевую модель* пользователя и принимает решения на основе его прав доступа. **Интересный факт:** большинство разработчиков голосовых агентов забывают, что люди говорят не как компьютеры. Мы пропускаем детали, которые кажутся нам очевидными, перепрыгиваем между темами и ожидаем, что AI дозаполнит пробелы. Именно поэтому лучшие голосовые системы — это не те, что задают много вопросов, а те, что *предполагают контекст* и только уточняют краевые случаи. Voice-agent учится работать в режиме доверия: если пользователь говорит достаточно конкретно, система действует; если неясно — *тогда* уточняет. **Итог:** Павел больше не получит сотню вопросов по поводу встречи. Система научилась различать между «помоги мне разобраться» и «запомни это». Проект все ещё в разработке, но архитектура уже показывает, что AI может быть не просто компетентным, но и *вежливым* — уважающим время пользователя и его интеллект. Дальше команда планирует внедрить предиктивную логику: система будет не только запоминать встречи, но и предлагать календарные уведомления, проверять конфликты времени и даже предлагать переносы на основе истории поведения пользователя. Но это уже совсем другая история. 😄 Что общего у yarn и подростка? Оба непредсказуемы и требуют постоянного внимания.
Когда AI становится парным программистом: история Claude Code
# Claude Code встречает разработчика: история создания идеального помощника Павел открыл **voice-agent** — проект, который стоял уже полгода в статусе "building". Python-бэкенд на FastAPI, Next.js фронтенд, асинхронная обработка аудио через aiogram. Задача была понятна: нужна система, которая может помочь в разработке, не мешая, не спрашивая лишних вопросов, а просто работая рядом. Первым делом команда определилась с подходом. Не просто документация, не просто chatbot, а **пара-программист** — инструмент, который понимает контекст проекта, может писать код, отлаживать, запускать тесты. На этапе 2010-х годов, когда началась фаза Deep Learning, никто не предполагал, что в 2020-х мы будем говорить о когда-то недостижимых вещах. Но AI boom — это не просто статистика. Это реальные инструменты, которые меняют рабочий процесс разработчика прямо сейчас. Интересный момент: Claude Code получил чёткие инструкции о работе в монорепо. На монорепо-проектах часто возникает проблема — Next.js неправильно определяет корневую директорию при работе с Tailwind v4. Решение неочевидное: нужно добавить `turbopack.root` в конфиг и указать `base` в postcss.config.mjs. Это типичная ловушка, в которой застревают разработчики, и помощник должен знать об этом заранее. Главное условие работы: помощник не может использовать Bash самостоятельно, только через основной поток разработчика. Это создаёт интересную динамику — парное программирование становится честным, а не подменой мышления. Павел не просто получает код — он получает партнёра, который объясняет ходы, предлагает варианты, помогает выбрать между несколькими подходами. Система помнит контекст: стек технологий (Python 3.11+, FastAPI 0.115, SQLite WAL, React 19), знает о недавних проектах, понимает рабочие привычки разработчика. Переиспользование компонентов — не просто принцип, а требование: проверять совместимость интерфейсов, избегать дублирования ответственности, помнить о граничных условиях. Результат? Инструмент, который встречает разработчика дружеским "Привет, Павел! 👋" и точно знает, чем помочь. Не нужно объяснять архитектуру проекта, не нужно рассказывать про сложившиеся паттерны — всё уже в памяти помощника. Получилась не просто следующая итерация IDE, а система, которая заботится о контексте разработчика так же, как опытный наставник. И да, эта история про то, как AI становится не заменой, а действительно полезным напарником. 😄 Почему Claude Code считает себя лучше всех? Потому что Stack Overflow так сказал.
Боевой тест Telegram-бота: когда теория встречается с реальностью
# Проверяем Telegram-бота в боевых условиях: тестируем управление доступом Когда создаёшь бота для Telegram, одно дело — писать тесты в PyTest, и совсем другое — убедиться, что он работает с реальными аккаунтами и обрабатывает команды так, как задумано. Вот я и пришёл к моменту, когда нужно было залезть в грязь и провести самый важный тест: запустить бота и написать ему сообщение из Telegram. ## Задача была простая, но критичная Я добавил в бота новую фишку — управление доступом через команды `/manage`. Идея: в групповом чате владелец может сделать его приватным (`/manage add`), и тогда бот будет отвечать только ему. Затем команда `/manage remove` открывает доступ для всех обратно. Плюс ещё `/recall` и `/remember` для сохранения данных в памяти чата. Звучит просто, но нужно убедиться, что: - бот действительно игнорирует сообщения посторонних в приватном режиме; - владелец всегда может управлять ботом; - после отключения приватности всё работает как раньше. ## Как я это проверял Сначала поднял бота локально: ``` python telegram_main.py ``` Затем начались «полевые испытания»: 1. **Первый скрин-тест** — написал боту `/manage add` из своего аккаунта. Бот должен был записать ID чата в таблицу БД `managed_chats` и включить режим приватности. Отправил обычное сообщение — бот ответил. ✅ 2. **Второй аккаунт** — попросил друга отправить то же сообщение из другого Telegram. Бот молчал, хотя обычно отвечает на всё. Middleware `permission_check.py` срабатывал корректно и блокировал обработку. ✅ 3. **Финальный тест** — написал `/manage remove` и снова попросил друга отправить сообщение. На этот раз бот ответил. Приватность снята, всё доступно. ✅ ## Что оказалось неочевидным Интеграционное тестирование в Telegram выявило одну тонкость: когда ты работаешь с асинхронным обработчиком команд в aiogram, timing-зависимые проверки могут создавать гонки условий. У меня был момент, когда команда `/manage add` срабатывала, но middleware проверял доступ *до* того, как запись попала в БД. Пришлось добавить небольшой await после insert'а, чтобы гарантировать консистентность. Ещё обнаружил: если ты работаешь с SQLite и одновременно несколько обработчиков пишут в таблицу, нужен явный `commit()` или использовать контекстный менеджер транзакций. Иначе другой процесс не увидит изменения до commit'а. ## Что дальше? После успешных интеграционных тестов я задокументировал всё в README.md — добавил секцию про управление доступом с примерами команд. Создал отдельный файл `docs/CHAT_MANAGEMENT.md` с полной архитектурой ChatManager, схемой БД и API reference для всех методов класса. Теперь у меня есть надёжная система для создания приватных чатов с ботом. Это можно использовать для ассистентов, которые работают с конфиденциальными данными, или для модераторов в больших группах. Главный вывод: прежде чем гордиться unit-тестами, обязательно проверь свой код в реальной среде. Иногда именно там появляются неожиданные проблемы, которые не поймать никаким PyTest. 😄 Что общего у Telegram API и инструкций по использованию телефона? И то, и другое люди игнорируют в пользу метода проб и ошибок.
Монорепо, голос и журнал ошибок: как AI учится не ломать код
# Когда AI-помощник встречается с монорепо: отладка голосового агента Проект `voice-agent` — это амбициозная задача: связать Python-бэкенд с Next.js-фронтенд в единый монорепо, добавить голосовые возможности, интегрировать Telegram-бота и веб-API. Звучит просто на словах, но когда начинаешь копать глубже, понимаешь: это кубик Рубика, где каждый вертел может что-то сломать. **Проблема, с которой я столкнулся, была банальной, но коварной.** Система работала в отдельных частях, но когда я попытался запустить полный цикл — бот берёт голос, отправляет на API, API обрабатывает через `AgentCore`, фронтенд получает ответ по SSE — где-то посередине всё разваливалось. Ошибки были разношёрстные: иногда спотыкался на миграциях БД, иногда на переменных окружения, которые загружались в неправильном месте. **Первым делом я понял: нужна система для документирования проблем.** Создал `ERROR_JOURNAL.md` — простой журнал "что сломалось и как это чинилось". Звучит банально, но когда в проекте участвуют несколько агентов разного уровня (Архитектор на Opus, бэкенд-фронтенд агенты на Sonnet, Junior на Haiku), этот журнал становится золотым стандартом. Вместо того чтобы каждый агент наново натыкался на баг с Tailwind v4 в монорепо, теперь первым делом смотрим журнал и применяем известное решение. **Архитектура обработки ошибок простая, но эффективная:** 1. Ошибка возникла → читаю `docs/ERROR_JOURNAL.md` 2. Похожая проблема есть → применяю известное решение 3. Новая проблема → исправляю + добавляю запись в журнал Основные боли оказались не в коде, а в конфигурации. С Tailwind v4 нужна магия в `next.config.ts` и `postcss.config.mjs` — добавить `turbopack.root` и `base`. SQLite требует WAL-режим и правильный путь к базе. FastAPI любит, когда переменные окружения загружаются только в точках входа (`telegram_main.py`, `web_main.py`), а не на уровне модулей. **Интересный момент: я переоценил сложность.** Большинство проблем решались не рефакторингом, а правильной организацией архитектуры. `AgentCore` — это единое ядро бизнес-логики для бота и API, и если оно валидируется с одной строки (`python -c "from src.core import AgentCore; print('OK')"`), весь стек работает как часы. **Итог:** система работает, но главный урок не в технических трюках — в том, что монорепо требует прозрачности. Когда каждая составляющая (Python venv, Next.js сборка, миграции БД, синхронизация переменных окружения) задокументирована и протестирована, даже сложный проект становится управляемым. Теперь каждый новый агент, который присоединяется к проекту, видит ясную картину и может сразу быть полезным, вместо того чтобы возиться с отладкой. На следующем этапе плотнее интегрирую streaming через Vercel AI SDK Data Stream Protocol и расширяю систему управления чатами через новую таблицу `managed_chats`. Но это — уже другая история. 😄 Что общего у монорепо и парка развлечений? Оба требуют хорошей разметки, иначе люди заблудятся.
Одна БД для всех: как мы добавили чаты без архитектурного хаоса
# Одна база на всех: как мы добавили управление чатами без архитектурного хаоса Когда проект растёт, растут и его аппетиты. В нашем Telegram-боте на основе Python уже была отличная инфраструктура — `UserManager` для управления пользователями, собственная SQLite база в `data/agent.db`, асинхронные запросы через `aiosqlite`. Но вот беда: чат-менеджер ещё не появился. А он нам был нужен. Стояла вот какая задача: нужно отслеживать, какие чаты управляет бот, кто их владелец, какой это тип чата (приватный, группа, супергруппа, канал). При этом не создавать отдельную базу данных — это же кошмар для девопса — а переиспользовать существующую инфраструктуру. **Первым делом** заглянул в текущую архитектуру. Увидел, что всё уже завязано на одной БД, один конфиг, одна логика подключения. Идеально. Значит, нужна просто одна новая таблица — `managed_chats`. Задумал её как простую структуру: `chat_id` как первичный ключ, `owner_id` для связи с пользователем, `chat_type` с проверкой типов через `CHECK`, поле `title` для названия и JSON-колонка `settings` на будущее. Обычно на этом месте разработчик бы создал абстрактный `ChatRepository` с двадцатью методами и паттерном `Builder`. Я же решил сделать проще — скопировать философию `UserManager` и создать классический `ChatManager`. Три-четыре асинхронных метода: добавить чат, проверить, управляется ли он, получить владельца. Всё на `aiosqlite`, как и везде в проекте. **Неожиданно выяснилось**, что индексы — это не украшение. Когда начну искать чаты по владельцу, индекс на `owner_id` будет спасением. SQLite не любит полные скены таблиц, если можно обойтись поиском по индексу. Интересный момент: SQLite часто недооценивают в стартапах, думают, что это игрушка. На самом деле она справляется с миллионами записей, если её правильно использовать. Индексы, `PRAGMA` для оптимизации, подготовленные statements — и у вас есть боевая база данных. Многие проекты потом переходят на PostgreSQL только потому, что привыкли к MySQL, а не из реальной нужды. В итоге получилась чистая архитектура: одна БД, одна точка подключения, новая таблица без какого-либо дублирования логики. `ChatManager` живёт рядом с `UserManager`, используют одни и те же библиотеки и утилиты. Когда понадобятся сложные запросы — индекс уже есть. Когда захотим добавить настройки чата — JSON-поле ждёт. И никаких лишних микросервисов. Следующий шаг — интегрировать это в обработчики событий Telegram API. Но это уже другая история. 😄 Почему база данных никогда не посещает вечеринки? Её постоянно блокирует другой клиент!
AI изучает себя: как мы мониторим научные тренды
# Когда AI исследует сам себя: как мы строили систему мониторинга научных трендов Вот уже несколько недель я сидел над проектом **trend-analisis** и постепенно понимал: обычный парсер научных статей — это скучно и малоэффективно. Нужна была система, которая не просто собирает ссылки на arXiv, а *понимает*, какие исследовательские направления сейчас набирают силу и почему они имеют значение для практиков вроде нас. Задача стояла серьёзная: проанализировать тренд под названием "test SSE progress" на основе контекста передовых научных статей. Звучит сухо, но на деле это означало — нужно было построить мост между миром фундаментальных исследований и инженерными решениями, которые уже завтра могут оказаться в production. ## Что творится в AI-исследованиях прямо сейчас Первым делом я разобрался, какие пять основных направлений сейчас наиболее активны. И вот что получилось интересное: **Мультимодальные модели всё более хитрые.** Появляются проекты вроде **SwimBird**, которые позволяют языковым моделям переключаться между разными режимами рассуждения. Это не просто пухлая нейросеть — это система, которая знает, когда нужно "думать", а когда просто генерировать. **Геометрия — это новый король.** Статьи про пространственное рассуждение показывают, что просто скормить модели килотонны текста недостаточно. Нужны геометрические приоры, понимание 3D-сцен, позиции камер. Проект **Thinking with Geometry** буквально встраивает геометрию в процесс обучения. Звучит как философия, но это работает. **Retrieval-системы перестают быть простыми.** Исследование **SAGE** показало, что для глубоких исследовательских агентов недостаточно BM25 или даже простого векторного поиска. Нужны умные retriever'ы, которые сами знают, что ищут. **Дешёвые модели становятся умнее.** Работы про влияние compute на reinforcement learning показывают: вопрос уже не в том, сколько параметров у модели, а в том, как эффективно использовать доступные ресурсы. Это открывает путь к edge AI и мобильным решениям. **Generative модели наконец-то становятся теоретически понятнее.** Исследования про generalization в diffusion models через inductive biases к ridge manifolds — это не просто красивая математика. Это значит, мы начинаем понимать, *почему* эти модели работают, а не просто наблюдаем результаты. ## Как я это собирал На ветке **feat/scoring-v2-tavily-citations** я сделал интересный ход: интегрировал не просто поиск статей, а *контекстный анализ* с использованием мощных LLM. Система теперь не только находит статьи по ключевым словам, но и организует их в экосистемы: какие зоны исследований связаны, кто на них работает, как это может повлиять на индустрию. Неожиданно выяснилось, что самая сложная часть — не техническая. Это правильно определить связи между соседними трендами. Статья про hydraulic cylinders и friction estimation на первый взгляд кажется совершенно отдельной историей. Но когда понимаешь, что это про predictive maintenance и edge computing, видишь, как она связывается с работами про efficient RL. Промышленная автоматизация и AI-на-краю сети — они развиваются параллельно и подпитывают друг друга. ## Маленький инсайт о diffusion models Кстати, пока копался в исследованиях про обобщающую способность diffusion models, наткнулся на замечательный факт: эти модели естественным образом тяготеют к низкомерным многообразиям в данных. Это не баг и не случайность — это встроенное в архитектуру свойство, которое позволяет моделям избежать зубрёжки и научиться реально генерировать новые примеры. Вот такое вот невидимое мастерство работает под капотом. ## Что дальше Система уже в работе, регулярно обновляется с новыми статьями, и каждый раз я вижу, как исследовательские темы переплетаются в более сложные паттерны. Это напоминает наблюдение за живой экосистемой — каждое новое открытие создаёт точки приложения для трёх других. Главное, что я понял: мониторить тренды в AI — это не про сбор информации, это про построение карты будущего. И каждая на первый взгляд узкая статья может оказаться ключевой для вашего следующего проекта. 😄 Обед разработчика: ctrl+c, ctrl+v из вчерашнего меню.
Очередь событий вместо словаря: как спасти архитектуру
# От очереди событий к стабильной архитектуре: как мы чинили `_fix_headings` в trend-analysis Есть у нас проект **trend-analysis** — сервис, который анализирует тренды и работает с длинными асинхронными операциями. Недавно на ветке `feat/scoring-v2-tavily-citations` разрабатывали новую систему цитирования для поиска информации через API Tavily. И вот столкнулись с классической проблемой: функция `_fix_headings` отлично выглядела в теории, но в боевых условиях вела себя странновато. Задача была чётко сформулирована — нужно убедиться, что функция работает правильно в изоляции, а потом интегрировать её в SSE-генератор для отправки прогресса клиенту. Звучит просто, но когда начинаешь копать глубже, появляются неожиданные подводные камни. Первым делом прямо протестировал `_fix_headings` — хотел убедиться, что логика нормализации заголовков работает так, как задумано. Потом понял, что архитектура прогресса была криво спроектирована. Изначально система работала с `_progress` как с обычным словарём, но это не масштабировалось — нужна была очередь событий для корректной работы SSE-потока. Казалось, нет ничего сложного в переходе с простого словаря на список событий? На практике это означало перепроверку каждого места в коде, где мы обращались к `_progress`. Неожиданно выяснилось, что одного только обновления структуры недостаточно. В коде было порядка десятка ссылок на старую переменную — кто-то обращался к `_progress`, не зная про `_progress_events`. Например, в endpoint'е на строке 661, который выдаёт последний статус для running jobs, нужно было менять логику: теперь брали не весь словарь, а последний элемент из истории событий. Это критично для получения актуального состояния операции. Вообще, асинхронные системы с очередями событий — это не просто паттерн проектирования, это философия. Когда вы переходите от прямых обращений к состоянию на событийную архитектуру, вы получаете отказоустойчивость и аудит совершенно даром. Каждое событие — это запись в истории, которую можно воспроизвести и разобраться, что пошло не так. Минус только один: нужно везде помнить об этом и не впадать в соблазн «быстро обратиться к состоянию напрямую». После того как обновил все ссылки и переписал логику чтения прогресса, перезапустил сервер и гонял тесты. `_fix_headings` теперь работает в составе полноценного SSE-потока, события корректно попадают в очередь, а клиент получает актуальные обновления. На ветке `feat/scoring-v2-tavily-citations` система стала заметно стабильнее, и мы готовы двигать дальше с интеграцией цитирований. Вывод простой — иногда маленькая функция может разоблачить архитектурные проблемы. Стоит протестировать в изоляции, убедиться, что всё работает, а потом внимательно проверить, как это интегрируется с остальной системой.
Двойная аутентификация: когда два охранника мешают друг другу
# Двойная защита убивает саму себя: как я развязал узел конфликтующей аутентификации Задача стояла простая на первый взгляд: запустить Management UI для проекта **borisovai-admin**. Казалось, что админ-панель встанет и будет работать. Но когда я подключил её к боевой инфраструктуре, выяснилось нечто интересное — UI запустилась, но пройти аутентификацию было невозможно. ## Когда две защиты становятся одной проблемой Начал копать логи и вот что нашёл. В инфраструктуре уже была слоёная защита: **Traefik** с плагином **ForwardAuth** отправлял все запросы на **Authelia** для двухфакторной аутентификации. Это первый уровень охраны — на уровне прокси. Здесь логика простая: если запрос идёт на `admin.borisovai.tech`, Traefik вежливо перенаправляет пользователя в Authelia. Но когда я добавил Management UI с встроенной OIDC-аутентификацией через **express-openid-connect**, произошло вот что: пользователь уже прошёл Authelia на уровне Traefik, но Management UI не поверил ему и снова отправил на Authelia через OIDC. Два редиректа подряд — и браузер начинает петлять между разными провайдерами аутентификации. Типичная ситуация, когда каждый охранник требует личный документ, не доверяя соседу. ## Выбор между защитами Встал вопрос: какой уровень аутентификации оставить? Отключить Traefik ForwardAuth? Отключить OIDC в Management UI? Или искать способ их синхронизировать? Я выбрал проверенный путь — **оставить Traefik ForwardAuth как основную защиту**, а OIDC отключить. Логика здесь такая: раз у нас уже есть надёжная защита на уровне прокси с поддержкой 2FA через Authelia, зачем добавлять второй слой? Внутри же Management UI я оставил **legacy session** — простую аутентификацию по логину и паролю. Получилось двухуровневое решение, но на разных слоях: внешняя защита через прокси и внутренняя через сессию. После изменений Management UI перезапустился без OIDC-интеграции. Теперь схема работает так: вы входите в `https://admin.borisovai.tech`, Traefik перенаправляет вас в Authelia, вы проходите двухфакторную аутентификацию, а потом попадаете на страницу логина самой админ-панели, где вводите учётные данные Management UI. ## Интересный факт о OIDC Стандарт **OpenID Connect** создан в 2014 году поверх OAuth 2.0 именно для решения проблем единого входа. Но мало кто знает, что OIDC работает лучше всего, когда он — **единственный** поставщик идентификации в системе. Как только вы пытаетесь слоить несколько провайдеров, начинаются конфликты. Классическая ловушка — стараться защитить приложение со всех сторон и получить вместо этого лабиринт редиректов. ## Неприятный бонус: проблема с `.ru` доменами Во время работы я обнаружил, что A-записи для `admin.borisovai.ru` и `auth.borisovai.ru` не добавлены у регистратора IHC. Let's Encrypt не может выдать сертификаты для доменов, которых нет в DNS. Решение пришло быстро — нужно добавить эти A-записи в панели регистратора, указывая на IP `144.91.108.139`. Казалось бы, мелочь, но именно такие детали часто становятся причиной того, что production не поднимается. ## Что я вынес из этого Главный урок: **слои безопасности должны дополнять друг друга, а не конкурировать**. Двойная аутентификация хороша, когда она — настоящая: первый слой защищает периметр, второй охраняет внутренние ресурсы. Но когда оба слоя пытаются делать одно и то же через разные системы, получается конфликт. Теперь Management UI работает, защита работает, и никто не просит удостоверения дважды. Инфраструктура проекта borisovai-admin стала на один уровень надёжнее. 😄 Почему Prometheus не пришёл на вечеринку? Его заблокировал firewall.
DNS-кеш и фантомный поддомен: охота на NXDOMAIN
# Когда DNS кеш становится врагом: охота на фантомный поддомен Работаю над проектом **borisovai-admin** — админ-панелью с собственной системой аутентификации. Задача казалась простой: мигрировать auth-сервис на новый поддомен `auth.borisovai.tech` и убедиться, что всё резолвится корректно. Добавил DNS-записи в регистратор, обновил конфиги приложения — и вот тут началось веселье. ## Первый знак беды Первая проверка через Google DNS (`8.8.8.8`) показала идеальный результат: `auth.borisovai.tech` резолвился на `144.91.108.139` без проблем. Казалось бы, всё готово. Но когда я переключился на **AdGuard DNS** (`94.140.14.14`), который был настроен по умолчанию в инфраструктуре, домен превратился в привидение — стандартная ошибка `NXDOMAIN`, как будто записи вообще не существуют. А вот `admin.borisovai.tech` спокойно резолвился везде. Значит, проблема именно с `auth.*`. Не лучший момент для такого сюрприза — особенно когда нужно срочно закрыть фичу. ## Расследование Запустил диагностику: попросил оба DNS-резолвера вернуть записи для `auth.borisovai.tech` и `auth.borisovai.ru`. Результат совпадал: Google видел, AdGuard не видел. Явный паттерн. Тут меня осенило — это же **отрицательный кеш DNS**! Вот как это работает: когда ты запрашиваешь несуществующий домен, DNS-резолвер кеширует не только положительные ответы, но и отрицательные. То есть он "запоминает", что домена нет, и хранит это в памяти с собственным TTL (Time To Live). У AdGuard это может быть час или даже дольше. Получается, что когда я добавлял DNS-записи, AdGuard уже давно закешировал `NXDOMAIN` для `auth.borisovai.tech`. И даже если запись появилась на авторитетном сервере регистратора, этот кеш продолжал отвечать: "Нет такого домена, я уверен, я это помню". ## Как я выбрался Вариант первый — просто ждать. AdGuard истечёт кеш, и всё чудо-образом заработает. Но тестировать нужно было *прямо сейчас*. Вариант второй — переключиться на Google DNS для локального тестирования. Работает мгновенно, но это временный костыль. Вариант третий — очистить локальный кеш операционной системы. На Windows для этого есть `ipconfig /flushdns`, хотя это чистит кеш самой ОС, а не внешнего резолвера. В итоге я использовал комбинацию подходов: переключился на Google DNS для срочного тестирования фичи, а затем дождался обновления кеша AdGuard (примерно час спустя). Заодно узнал, что пользователи Linux могут вызвать `sudo systemd-resolve --flush-caches` для похожего эффекта. ## Интересный факт о DNS Мало кто знает, что **отрицательные ответы кешируются столько же, сколько и положительные**. Оба имеют собственный TTL, обычно от 300 до 3600 секунд. Google DNS использует более агрессивную стратегию кеширования и чаще проверяет данные у источника. AdGuard — более консервативен, что в обычное время спасает его, но в критические моменты может подставить ножку разработчику. ## Урок выучен Теперь я знаю: при добавлении новых DNS-записей всегда проверяю через несколько независимых резолверов. Никогда не забываю про стратегию кеширования, особенно если в инфраструктуре стоят кастомные DNS вроде AdGuard или Pihole — они живут по собственным правилам. И да, теперь я знаю точное место, где искать, если история повторится. А повторится ещё не раз. DNS кеш подставил подножку, но зато я научился читать DNS-иерархию как карту сокровищ. Что общего у AdGuard DNS и кота? 😄 Оба игнорируют инструкции и делают только то, что хотят.
DNS кеш подставил подножку: охота на фантомный домен
# Когда DNS кеш становится врагом: охота на призрачный домен в проекте borisovai-admin Казалось бы, добавил DNS-записи для `auth.borisovai.tech` — и готово. Но нет. Домен упорно не резолвился с одного DNS-сервера, зато с другого всё работало как часы. Началась охота. ## Первые подозрения Работаю над проектом **borisovai-admin** — админ-панель с собственной системой аутентификации. Задача простая: перенести auth-сервис на новый поддомен и убедиться, что всё резолвится. Добавил записи в регистратор, обновил конфиги — и вот тут началось веселье. Первый звонок: с Google DNS (`8.8.8.8`) всё отлично. `auth.borisovai.tech` резолвится на `144.91.108.139`. Но когда переключился на **AdGuard DNS** (`94.140.14.14`), который был настроен по умолчанию, домен превратился в привидение — `NXDOMAIN`, записи как будто не существуют. А вот `admin.borisovai.tech` спокойно резолвился везде. Что-то не так с `auth.*`. ## Расследование Началось с простого: запросить обе версии домена через оба резолвера. `auth.borisovai.tech` и `auth.borisovai.ru` вели себя одинаково — видны у Google, невидимы у AdGuard. Явный признак того, что записи в регистраторе были добавлены *после* того, как AdGuard их закешировал. Вот в чём суть: когда запрашиваешь несуществующий домен, DNS-резолвер кеширует отрицательный ответ (`NXDOMAIN`) на какое-то время. Даже если позже ты добавишь запись, старый кеш будет отправлять безутешный "нет такого домена". У AdGuard этот кеш может жить до часа. ## Как я это решил Вариант первый — просто подождать. AdGuard истечёт кеш, записи проявятся сами. Но тестировать нужно было *сейчас*. Вариант второй — переключиться на Google DNS. Работает мгновенно, но это костыль. Вариант третий — очистить локальный кеш на машине. В Windows команда `ipconfig /flushdns` чистит кеш операционной системы, а не самого DNS-резолвера. Но иногда помогает. На самом деле я использовал комбинацию: временно переключился на Google DNS для тестирования, а затем дождался, пока AdGuard обновит свои данные. ## Интересный факт о DNS Мало кто знает, что DNS-записи имеют собственное поле `TTL` (Time To Live) — "время жизни" в кеше. По умолчанию обычно ставят 3600 секунд (час). Google использует более агрессивную стратегию кеширования, AdGuard — более консервативную. Вот поэтому один резолвер сразу видит новую запись, а другой ещё час её "забывает". ## Вывод Простой урок: при добавлении новых DNS-записей всегда проверяй через несколько резолверов. Если в сети настроены кастомные DNS (как AdGuard или Pihole), они могут сыграть с тобой в злую шутку. И никогда не забывай про `ipconfig /flushdns` или `sudo systemd-resolve --flush-caches` на Linux — иногда это спасает часы дебага. Дальше — уже знаю, где искать, если история повторится. А повторится ещё не раз. Что общего у Netlify и кота? 😄 Оба делают только то, что хотят, и игнорируют инструкции.
DNS и кэш: охота на запись, которая вроде есть, но не видна
# Охота на привидения в DNS: как потерянная запись чуть не сломала аутентификацию Проект `borisovai-admin` — это админ-панель с полноценной системой аутентификации. Задача казалась простой: настроить поддомены для разных частей приложения. `admin.borisovai.tech` уже работал безупречно, а вот `auth.borisovai.tech` и `auth.borisovai.ru` упорно отказывались резолвиться. Казалось, что может быть проще — добавить записи и забыть? Не так всё оказалось. Первым делом я начал с базовой диагностики. Проверил `nslookup` и `dig` — и вот тут началось веселье. `auth.borisovai.tech` **не резолвился с локального DNS**, а вот Google DNS (8.8.8.8) прекрасно возвращал `144.91.108.139`. Это был явный признак того, что проблема не в глобальной DNS-иерархии, а где-то рядом. Полез в DNS API — может, записи просто не создались? Нашёл в базе пусто: `records: []`. Автоматического создания не было. Но вот странный момент — `admin.borisovai.tech` работал без проблем. Почему одна запись есть в DNS API, а другая нет? Начал разбираться в истории создания записей. Выяснилось, что **`admin.borisovai.tech` был добавлен напрямую у регистратора**, минуя DNS API. А вот `auth.*` я стал добавлять через API, и тут столкнулся с интересным поведением: локальный AdGuard DNS (94.140.14.14), который использовался как основной рекурсивный резолвер, **кэшировал старые данные** или просто не видел новые записи из-за задержки распространения. Это была классическая ловушка DNS-администраторов: разные пути создания записей приводят к рассинхронизации. Когда у тебя есть несколько источников истины (регистратор, API, локальный кэш), они начинают рассказывать разные истории. Сервис аутентификации попадал на несуществующий адрес, и всё падало в момент, когда требовалась верификация токена. **Интересный факт**: DNS работает на принципе давности — записи имеют TTL (Time To Live), и если кэширующий резолвер уверен, что всё верно, он будет возвращать старые данные до истечения TTL, даже если на авторитативном сервере уже всё изменилось. В нашем случае TTL был достаточно высоким, поэтому AdGuard упорно держался за мысль, что `auth.borisovai.tech` не существует. Решение: я привёл все записи в единую систему — создал их через DNS API, настроил правильные TTL (600 секунд вместо 3600) и добавил явное перенаправление с `auth.borisovai.ru` на основной домен. Проблема испарилась. Главный урок: **в распределённых системах всегда проверяй, откуда берётся каждый кусок информации**. DNS выглядит просто, пока не начнёшь складывать вместе мнения разных серверов. И да, не забывай очищать кэш после изменений — Google DNS обновляется быстрее, чем AdGuard, и это сейчас спасало жизнь. 😄 **А знаешь, почему DNS никогда не ходит к психологу?** Потому что у него всё равно проблемы с разрешением.
Туннели и таймауты: управление инфраструктурой в админ-панели
# Туннели, Traefik и таймауты: как мы добавили управление инфраструктурой в админ-панель Проект **borisovai-admin** рос не по дням, а по часам. Сначала была одна машина, потом две, потом стало ясно — нужна нормальная система для управления сетевыми туннелями между серверами. Задача выглядела острой: юзеру нужен интерфейс, чтобы видеть, какие туннели сейчас активны, создавать новые и удалять старые. Без этого администрирование превращалось в ручную возню с конфигами на каждой машине. Первое решение было логичным: взял **frp** (Fast Reverse Proxy) — лёгкий инструмент для туннелирования, когда сервер скрыт за NAT или брандмауэром. Почему не что-то более «облачное»? Потому что здесь нужна полная контроль, минимум зависимостей и максимум надёжности. FRP ровно это даёт. Спроектировал веб-интерфейс: добавил страницу `tunnels.html` с простеньким списком активных туннелей, кнопками для создания и удаления. На бэкенде в `server.js` реализовал пять API endpoints для управления состоянием. Параллельно обновил скрипты инсталляции: `install-all.sh` и отдельный `install-frps.sh` для развёртывания FRP сервера, плюс `frpc-template` для клиентов на каждой машине. Не забыл навигационную ссылку «Туннели» на всех страницах админ-панели — мелочь, но юзабилити взлетела. Вроде всё шло гладко, но потом началось. Пользователи начали скачивать большие файлы через GitLab, и соединение рубилось где-то в середине процесса. Проблема оказалась в **Traefik** — наш обратный прокси по умолчанию использует агрессивные таймауты. Стоило файлу загружаться дольше пары минут — и всё, соединение закрыто. Пришлось углубиться в конфиги Traefik. Установил `readTimeout` в 600 секунд (10 минут) и создал специальный `serversTransport` именно для GitLab. Написал скрипт `configure-traefik.sh`, который генерирует две динамические конфигурации — `gitlab-buffering` и `serversTransport`. Результат: файлы теперь загружаются спокойно, даже если это полгигабайта архива. **Интересная особенность Traefik:** это микросервис-балансировщик, который позиционируется как облегчённое решение, но на практике требует хирургической точности при настройке. Неправильный таймаут — и приложение выглядит медленным. Правильный — и всё летает. Один параметр, и мир меняется. Параллельно реорганизовал документацию: разбил `docs/` на логические части — `agents/`, `dns/`, `plans/`, `setup/`, `troubleshooting/`. Добавил полный набор конфигов для конкретного сервера в `config/contabo-sm-139/` (traefik, systemd, mailu, gitlab) и обновил скрипт `upload-single-machine.sh` для их загрузки. За вечер родилась полноценная система управления туннелями с интерфейсом, автоматизацией и нормальной документацией. Проект теперь легко масштабируется на новые серверы. Главное, что узнал: **Traefik** — это не просто прокси, это целая философия правильной конфигурации микросервисов. Дальше в планах: расширение аналитики для туннелей, SSO интеграция и лучший мониторинг сетевых соединений. 😄 **Разработчик**: «Я настроил Traefik». **Пользователь**: «Отлично, тогда почему мой файл не загружается?» **Разработчик**: «А ты пробовал перезагрузить сервер?»
Traefik и опциональные middleware: война с зависимостями
# Когда конфиги кусаются: история про зависимые middleware в Traefik Проект `borisovai-admin` — это не просто админ-панель, это целая инфраструктурная система с аутентификацией через Authelia, обратным прокси на Traefik и кучей moving parts, которые должны работать в идеальной гармонии. И вот в один прекрасный день выясняется: когда ты разворачиваешь систему без Authelia, всё падает с ошибкой 502, потому что Traefik мечтательно ищет middleware `authelia@file`, которого просто нет в конфиге. **Завязка проблемы была в статических конфигах.** Мы жёстко прописали ссылку на `authelia@file` прямо в Traefik-конфигурацию, и это работало, когда Authelia установлена. Но стоило её отключить или не устанавливать вообще — бум, 502 ошибка. Получается, конфиги были сильно связаны с опциональным компонентом. Это классический случай, когда инфраструктурный код требует гибкости. Решение разбилось на несколько фронтов. Во-первых, **убрали жёсткую ссылку на `authelia@file` из статических конфигов Traefik** — теперь это просто не указывается в базовых настройках. Во-вторых, создали правильную цепочку инициализации. Скрипт `install-authelia.sh` теперь сам добавляет `authelia@file` в `config.json` и настраивает OIDC при установке. Скрипт `configure-traefik.sh` проверяет переменную окружения `AUTHELIA_INSTALLED` и условно подключает middleware. А `deploy-traefik.sh` перепроверяет на сервере, установлена ли Authelia, и при необходимости переустанавливает `authelia@file`. По ходу дела обнаружилась ещё одна проблема в `install-management-ui.sh` — там был неправильный путь к `mgmt_client_secret`. Исправили. А `authelia.yml` вообще выкинули из репозитория, потому что его всегда генерирует сам скрипт установки. Зачем держать в git то, что одинаково воспроизводится каждый раз? **Интересный момент про middleware в Docker-сообществе:** люди часто забывают, что middleware — это не просто функция, это *объект конфигурации*, который должен быть определён до использования. Traefik здесь строг: ты не можешь ссылаться на middleware, которого не существует. Это похоже на попытку вызвать функцию, которая не импортирована в Python. Простая ошибка, но очень болезненная в production-системах. **Итоговая архитектура** получилась намного гибче: система работает как с Authelia, так и без неё, конфиги не лежат мёртвым грузом в репо, инсталляторы действительно знают, что они делают. Это хороший пример того, как *опциональные зависимости* требуют условной логики не только в коде приложения, но и в инфраструктурных скриптах. Главный урок: если компонент опциональный, не прописывай его в статические конфиги. Пусть туда добавляются динамически при необходимости. 😄 Разработчик: «Я знаю Traefik». HR: «На каком уровне?». Разработчик: «На уровне количества 502 ошибок, которые я пережил».
SSO за выходные: как я запустил Authelia на боевом сервере
# Authelia в боевых условиях: как я собрал Single Sign-On за выходные Задача была амбициозная: в проекте **borisovai-admin** нужно было внедрить полноценную систему единой авторизации. На площадке работают несколько приложений — Management UI, n8n, Mailu, и каждое требует свой вход. Кошмар для пользователя и сущее издевательство над принципом DRY. Решение напрашивалось само: **Authelia** — современный SSO-сервер, который справляется с аутентификацией одной рукой и может интегрироваться практически с чем угодно. ## С чего я начал Первым делом создал `install-authelia.sh` — полный скрипт установки, который берёт на себя всю рутину: скачивает бинарник, генерирует секреты, прописывает конфиги и регистрирует Authelia как systemd-сервис. Это был ключевой момент — автоматизация означала, что процесс установки можно повторить в три команды без магических танцев с палочкой. Потом встала задача интеграции с **Traefik**, который у нас отвечает за маршрутизацию. Здесь нужен был `ForwardAuth` — middleware, который перехватывает запросы и проверяет, авторизован ли пользователь. Создал `authelia.yml` с настройкой ForwardAuth для `auth.borisovai.ru/tech`. Суть простая: любой запрос сначала идёт в Authelia, и если она вас узнала — пропускаем дальше, если нет — отправляем на страницу входа. ## Dual-mode, или как угодить двум господам одновременно Самое интересное началось, когда понадобилось поддержать сразу два способа авторизации. Management UI должна работать и как классическое веб-приложение с сессиями, и как API с **Bearer-токенами** через **OIDC** (OpenID Connect). Пришлось написать `server.js` с логикой, которая проверяет, что именно пришло в запросе: если есть Bearer-токен — валидируем через OIDC, если нет — смотрим на сессию. Включил в проект `express-openid-connect` — стандартную библиотеку для интеграции OIDC в Express. Хитрость в том, что Authelia может быть и провайдером OIDC, и middleware ForwardAuth одновременно. Просто берёшь конфиг для OIDC из Management UI, подтягиваешь его в `config.json` через автоопределение (этим займется `install-management-ui.sh`), и всё начинает работать как часы. ## Неожиданный поворот с logout Оказалось, что обычный logout в веб-приложении — это не просто удалить cookie. Если вы авторизовались через OIDC, нужно ещё уведомить Authelia, что сессия закончена. Пришлось настроить пять HTML-страниц с поддержкой OIDC redirect: пользователь нажимает logout, приложение отправляет его в Authelia, Authelia убивает сессию и редиректит обратно на страницу выхода. Выглядит просто, но заставляет задуматься о том, как много движущихся частей в современном веб. ## Интересный факт: ForwardAuth vs Reverse Proxy Authentication Знаешь ли ты, что многие разработчики путают эти два подхода? ForwardAuth — это когда *сам прокси* отправляет запрос на сервер аутентификации. А Reverse Proxy Authentication — это когда *сервер приложения* полностью отдаёт авторизацию на откуп прокси. Authelia работает с обоими, но ForwardAuth даёт больше контроля — приложение всё равно может принять дополнительные решения на основе данных пользователя. ## Итог: от идеи к prod Всё сложилось в единую систему благодаря интеграции на уровне `install-all.sh` — компонент `INSTALL_AUTHELIA` занимает шаг [7.5/10], что означает: это не первый день, но далеко не последний штрих. Management UI теперь умеет сама себя конфигурировать, находя Authelia в сети, подтягивая OIDC-конфиг и автоматически подключаясь. Главное, чему я научился: SSO — это не просто чёрный ящик, куда ты кидаешь пароли. Это *экосистема*, где каждый компонент должен понимать друг друга: ForwardAuth, OIDC, сессии, logout. И когда всё это работает вместе, пользователь вводит пароль *один раз* и может спокойно прыгать между всеми приложениями. Вот это да. Почему React расстался с разработчиком? Слишком много зависимостей в отношениях 😄
VPN отключился молча: как я потерял доступ к релизу
# Когда VPN молчит: охота на привидение среди ночи Пятница, конец дня, а на горизонте маячит дедлайн релиза **v1.0.0** проекта **speech-to-text**. Финальный рывок: нужно запушить коммит с автоматизацией сборки в master, создать тег и загрузить артефакт в GitLab Package Registry. Казалось бы, стандартная процедура — пара команд в консоль, и мы свободны. Но начало было не самым обнадёживающим. Я попытался перезапустить **Gitaly** — критический компонент GitLab, отвечающий за хранение репозиториев и работу с гитом на серверной стороне. SSH молчит. Попробовал достучаться через HTTP к самому GitLab-серверу — тишина. Весь сервер, похоже, вообще не существует с точки зрения моей машины. Стандартный алгоритм отладки: если ничего не отвечает, проблема либо с сервером, либо с сетью. Сервер на **144.91.108.139** физически жив, но почему-то недоступен. Проверяю VPN, и вот оно — диапазон **10.8.0.x** не найден. **OpenVPN отключился.** Просто тихо, без уведомления, выполнив свою работу и уйдя в отставку. Оказывается, весь этот вечер я сидел за стеной недоступности. Компания добавила слой безопасности, завернув внутреннюю инфраструктуру в защищённый туннель, а я, горя желанием запушить релиз, забыл про это самое VPN. Типичная история: инфраструктура дышит тебе в спину, а ты смотришь на экран и недоумеваешь, почему ничего не работает. **Интересный факт:** Gitaly создан именно для того, чтобы отделить операции с файловой системой от основного приложения GitLab. Это позволило компании масштабировать сервис горизонтально, но цена — жёсткая зависимость. Если Gitaly недоступен, GitLab попросту не может выполнять операции с гитом. Это как попытаться ходить с отключенными ногами. Решение было простым, но требовало действия. Нужно было переподключить **OpenVPN**, дождаться, пока туннель встанет на место, и выполнить `git push origin master`. После этого запустить скрипт релиза на Python, который собирает EXE из исходного кода, упаковывает в ZIP и загружает артефакт в Package Registry. Когда VPN восстановился, все лампочки загорелись в правильном порядке. Gitaly ожил, сервер откликнулся, и коммит с облегчением пошёл в master. Релиз уложился в срок. **Урок:** прежде чем копать проблему на сервере, убедитесь, что вы вообще до него дотягиваетесь. VPN, firewall, маршруты — всё это может спокойно жить в фоне, пока вы ловите ошибки в коде. Инфраструктура любит скрываться за слоями безопасности, и иногда самая сложная проблема решается одной переподключением. 😄 OpenVPN — как невидимая рука, которая отключается именно тогда, когда ты забываешь, что её держишь.
VPN отключился в самый неудачный момент
# Когда Gitaly молчит: охота на недоступный GitLab среди ночи Вечер пятницы, deadline на релиз `v1.0.0` проекта **speech-to-text** буквально под носом. Нужно было запушить финальный коммит с автоматизацией сборки в master, создать тег и загрузить артефакт в Package Registry. Казалось бы, стандартная процедура — клик, клик, и всё готово. Но началось всё с того, что я попытался перезапустить **Gitaly** на GitLab-сервере через SSH. Ничего не вышло. Сервер просто не отвечает. Ладно, попробую обойтись без SSH — может быть, сам GitLab доступен по HTTP? Нет, он тоже молчит как партизан. Вообще ничего не откликается. Паника? Нет, просто логика. Если сервер не отвечает ни на SSH, ни на HTTP, значит либо он упал, либо сетевая проблема. Проверяю VPN. И вот оно! IP-адрес в диапазоне `10.8.0.x` не найден. **OpenVPN отключился.** Сервер GitLab (`gitlab.dev.borisovai.tech`) размещён на машине `144.91.108.139`, которая доступна только через защищённый туннель. Вот это поворот! Оказывается, всё время я просто был за стеной недоступности — VPN выполнял свою работу, но потом тихо сдался. Компания добавила слой безопасности, а я про это забыл. Типичная история: инфраструктура дышит на тебе в спину, а ты смотришь на монитор и недоумеваешь. **Интересный факт:** Gitaly — это компонент GitLab, отвечающий за хранение репозиториев и работу с гитом на серверной стороне. Создан он специально для того, чтобы отделить операции с файловой системой от основного приложения. Если Gitaly недоступен, GitLab просто не может выполнять операции с гитом — это как отключить ноги при попытке ходить. Решение было простым, но требовало действий. Нужно было: 1. Подключить **OpenVPN** к серверу `144.91.108.139` 2. После восстановления туннеля выполнить `git push origin master` из ветки **master** 3. Запустить скрипт релиза: `.\venv\Scripts\python.exe scripts/release.py` Этот скрипт собирает EXE из Python-кода, упаковывает его в ZIP, загружает артефакт в GitLab Package Registry и создаёт тег версии. Когда VPN встал на место и лампочки начали загораться в правильном порядке — Gitaly вновь ожил, сервер откликнулся, а мой коммит с облегчением пошёл в master. Релиз ушёл в прод ровно в срок. **Урок на вечер:** прежде чем искать проблему на сервере, проверьте, что вы вообще до него дотягиваетесь. Инфраструктура любит прятаться за слоями безопасности, и иногда самая сложная проблема решается одной переподключением. 😄 Почему MongoDB считает себя лучше всех? Потому что Stack Overflow так сказал.
Race condition в системе версионирования: как два ревьюера поймали потерю данных
# Когда два ревьюера находят одни и те же баги: история о том, как система версионирования может потерять данные Работаешь над feature branch `feat/scoring-v2-tavily-citations` в проекте **trend-analisis**, пилишь систему многоуровневого анализа трендов. Задача звучит просто: позволить анализировать один тренд несколько раз с разными параметрами (`depth`, `time_horizon`), сохранять все варианты и отправлять их на фронт. Казалось бы, что может быть проще? Потом коммит отправляешь на ревью двум коллегам. И они оба, независимо друг от друга, находят одну и ту же **критическую ошибку** — race condition в функции `next_version()`. Момент волшебства: когда разные люди пришли к одному выводу, значит, ошибка точно смертельна. Вот что происходит. Функция `next_version()` считает максимальный номер версии анализа для тренда и возвращает `max + 1`. Звучит логично, но представь: два запроса одновременно анализируют один тренд. Оба вызывают `next_version()`, получают одинаковый номер (например, `version=3`), затем пытаются сохранить результат через `save_analysis()`. Один INSERT успешен, второй молча пропадает в чёрной дыре `except Exception: pass`. Данные потеряны, пользователь не узнает о проблеме. Но это ещё не всё. Коллеги заметили вторую проблему: функция видит только завершённые анализы (статус `completed`), поэтому запущенный анализ (статус `running`) остаётся невидимым для системы версионирования. Получается, что второй запрос стартует с того же номера версии, какой уже занят висящим процессом. Классическая ловушка асинхронности. Обнаружилось ещё несколько багов: фронт ожидает получить один объект `getAnalysisForTrend`, а бэкенд начал отправлять массив анализов. TypeScript тип `AnalysisReport` не знает про новые поля (`version`, `depth`, `time_horizon`, `parent_job_id`) — они приходят с сервера и сразу теряются. Параметр `parent_job_id` вообще ни на что не валидируется, что открывает дверь для инъекций. И `depth` может быть любым числом — никакого лимита, хоть 100 передай. **Интересный момент:** многие разработчики думают, что `except Exception: pass` это "временно", но на практике эта конструкция часто уходит в production как постоянное решение, маскируя критические ошибки. Это называется *exception swallowing*, и это один из самых подлых антипаттернов асинхронного кода. Решение оказалось не очень сложным, но требовало думать о транзакциях иначе. Нужно либо **переместить `next_version()` внутрь `save_analysis()`** с retry-логикой на `IntegrityError`, либо использовать **атомарный SQL-запрос `INSERT...SELECT MAX(version)+1`**, чтобы гарантировать уникальность версии за одно действие. Плюс резервировать версию сразу при старте анализа (INSERT со статусом `running`), чтобы параллельные запросы их видели. Для фронта пришлось добавить новый endpoint `getAnalysesForTrend` (а старый `getAnalysisForTrend` оставить для обратной совместимости). TypeScript типы расширены, валидация на `parent_job_id` добавлена, `depth` ограничен до 7 через `Pydantic Field(ge=1, le=7)`. Главный урок: **код, который "работает на примере", и код, который справляется с race conditions, это два разных животных**. Всегда думай про параллелизм, даже если сейчас система однопоточная. И когда два ревьюера независимо находят один и тот же баг — это не совпадение, это сигнал, что нужно переделывать архитектуру, а не чинить синтаксис. 😄 Prometheus: решение проблемы, о существовании которой ты не знал, способом, который не понимаешь.
Мелочь в навигации — архитектура на бэке
# Туннелировать админ-панель: когда мелочь оказывается архитектурой Проект **borisovai-admin** — это управленческая панель для социального паблишера. И вот однажды возникла потребность: нужна видимость в туннели FRP (Fast Reverse Proxy). Казалось — простая фича. Добавить ссылку в навигацию, создать эндпоинты на бэке, вывести данные на фронте. Четыре-пять дней работы, максимум. Началось всё с мелочи: требовалось добавить пункт "Туннели" в навигацию. Но навигация была одна, а HTML-файлов четыре — `index.html`, `tokens.html`, `projects.html`, `dns.html`. И здесь скрывалась первая ловушка: одна опечатка, одна невнимательность при копировании — и пользователь запутается, кликнув на несуществующую ссылку. Пришлось синхронизировать все четыре файла, убедиться, что ссылки находятся на одинаковых позициях в строках 195–238. Мелочь, которую легко упустить при спешке. Но мелочь эта потащила за собой целую архитектуру. На бэке понадобилось добавить две вспомогательные функции в `server.js`: `readFrpsConfig` — для чтения конфигурации FRP-сервера, и `frpsDashboardRequest` — для безопасного запроса к dashboard FRP. Это не просто HTTP-вызовы: это минимальная абстракция, которая облегчит тестирование и повторное использование. Затем пришлось вывести четыре GET-эндпоинта: статус сервера, список активных туннелей с метаинформацией, текущую конфигурацию в JSON и даже генератор `frpc.toml` для скачивания клиентского конфига в один клик. И вот неожиданно выяснилось — сам FRP-сервер ещё нужно установить и запустить. Обновил `install-all.sh`, добавил FRP как опциональный компонент: не все хотят туннели, но кто выбрал — получит полный стек. На фронте создал новую страницу `tunnels.html` с тремя блоками: карточка статуса (живой ли FRP), список туннелей с автообновлением каждые 10 секунд (классический полинг, проще WebSocket'а для этого масштаба) и генератор конфига для клиента. **Интересный факт**: полинг через `setInterval` кажется древним подходом, но именно он спасает от overengineering'а. WebSocket требует поддержки на обеих сторонах, fallback'и на старых браузерах, управление жизненным циклом соединения. Для обновления статуса раз в 10 секунд это overkill. Главное — не забыть очистить интервал при размонтировании компонента, иначе получишь утечку памяти и браузер начнёт отваливаться. Главный урок: даже в мелких фичах скрывается целая архитектура. Одна ссылка в навигации потребовала синхронизации четырёх файлов, пять эндпоинтов на бэке, новую страницу на фронте, обновление скрипта установки. Это не scope creep — это *discovery*. Лучше потратить час на планирование полной цепочки, чем потом переделывать интеграцию, когда уже половина team работает на основе твоей "быстрой фички". 😄 FRP — это когда твой сервер вдруг получает способность ходить в гости через NAT, как путник с волшебным клаком из мультика.
GitLab Pages выдал секреты: как мы чуть не залили артефакты в интернет
# Как мы защитили артефакты приватного проекта, но случайно выставили их в интернет Проект `speech-to-text` — это голосовой ввод для веб-приложения. Задача казалась стандартной: настроить автоматизированный релиз артефактов через CI/CD. Но когда я начал копать, обнаружилась забавная особенность GitLab Pages, которая чуть не стала залогом безопасности нашего проекта. ## Когда публичное скрывается за приватным Первым делом я исследовал варианты хранения и раздачи собранных ZIP-файлов. В репозитории всё приватное — исходники защищены, доступ ограничен. Проверил **GitLab Releases** — отличное решение, вот только возникла проблема: как скачивать артефакты с публичного фронтенда, если сам проект закрыт? Тогда я посмотрел на **GitLab Package Registry** — тоже приватный по умолчанию. Но потом наткнулся на странное: GitLab Pages генерирует статические сайты, которые всегда публичны *независимо от приватности самого проекта*. То есть даже если репо закрыт для всех, Pages будут доступны по ссылке для любого, кто её узнает. **Неожиданно выяснилось:** это не баг, а фича. Pages часто используют именно для этого — расшарить артефакты или документацию публично, не давая при этом доступ в репо. ## Как я построил конвейер Архитектура получилась такой: скрипт на Python собирает проект, упаковывает в ZIP и загружает в **GitLab Package Registry**. Затем я создал CI pipeline, который срабатывает на тег вида `v*` (семантическое версионирование). Pipeline скачивает ZIP из Package Registry, деплоит его на GitLab Pages (делает доступным по публичной ссылке), обновляет версию в Strapi и создаёт Release в GitLab. Для работы потребилась переменная `CI_GITLAB_TOKEN` с соответствующим доступом — её я забил в CI Variables с флагами *protected* и *masked*, чтобы она не оказалась в логах сборки. Версионирование жёсткое: версия указывается в `src/__init__.py`, оттуда её подхватывает скрипт при сборке. Архивы называются в стиле `VoiceInput-v1.0.0.zip`. ## Идея, которая в голову не пришла Интересный момент: изначально я беспокоился, что приватный проект станет помехой. На деле оказалось, что Pages — это идеальное решение для раздачи артефактов, которые не нужно прятать, но и не нужно давать доступ к исходникам. Классический сценарий для скачиваемых утилит, библиотек или сборок. Теперь релиз — это одна команда: `.\venv\Scripts\python.exe scripts/release.py`. Скрипт собирает, архивирует, загружает, пушит тег. А CI сам позаботится о Pages, Strapi и Release. 😄 Почему GitLab Pages пошёл в терапию? Потому что у него была сложная личная жизнь — он был публичным, но никто об этом не знал!
Тесты падают: как найти виновника, когда это наследство
# Когда тесты ломаются, но ты не виноват: история отладки в проекте trend-analysis Представь ситуацию: ты вносишь изменения в систему анализа трендов, коммитишь в ветку `feat/scoring-v2-tavily-citations`, запускаешь тесты — и вот, шесть тестов падают красными крестами. Сердце замирает. Первый вопрос: это моя вина или наследство от предков? ## Охота на виновника начинается В проекте `trend-analysis` я добавлял новые параметры к функции `_run_analysis`: `time_horizon` и `parent_job_id` как именованные аргументы с дефолтными значениями. На бумаге — всё обратно совместимо. На тестах — красный экран смерти. Первым подозреваемым стала функция `next_version()`. Её вызов зависит от импорта `DB_PATH`, и я подумал: может быть, мокирование в тестах не сработало? Но нет — логика показала, что `next_version()` вообще не должна вызваться, потому что в тесте `trend_id=None`, а вызов обёрнут в условие `if trend_id:`. Второй подозреваемый: `graph_builder_agent`. Эта штука вызывается со специальным параметром `progress_callback=on_zone_progress`, но её мок в тестах — просто лямбда-функция, которая принимает только один позиционный аргумент: ```python lambda s: {...} # вот так мокируют ``` А я вызываю её с дополнительным именованным аргументом. Лямбда восстаёт против `**kwargs`! ## Разворот сюжета Но подождите. Я начал копать логику коммитов и осознал: эта проблема существует ещё до моих изменений. Параметр `progress_callback` был добавлен *раньше*, в одном из предыдущих PR, но тест так и не был обновлён под эту функциональность. Я найденный баг не создавал, я его просто разбудил. Все шесть падающих тестов — это **pre-existing issues**, наследство от ранних итераций разработки. Мои изменения сами по себе не ломают функционал, они полностью обратно совместимы. ## Что дальше? Решили остановиться на стадии прототипа для валидации концепции. На бэкенде я уже поднял: - миграции БД для версионирования анализов (добавил `version`, `depth`, `time_horizon`, `parent_job_id`); - новые функции `next_version()` и `find_analyses_by_trend()` в `analysis_store.py`; - обновлённые Pydantic-модели в `schemas.py`; - API endpoints с автоинкрементом версий в `routes.py`. Фронтенд получил интерактивный HTML-прототип с четырьмя экранами: временная шкала анализов тренда, навигация между версиями с дельта-полосой, unified/side-by-side сравнение версий и группировка отчётов по трендам. ## Урок на память Когда читаешь падающий тест, первое правило: не сразу ищи свою вину. Иногда это старый долг проекта, который ждал своего часа. Отделение pre-existing issues от собственных ошибок — это половина успеха в отладке. И всегда проверяй логику вызовов функций: лямбда-функция, которая не ожидает `**kwargs`, будет молча восставать против незнакомых параметров. Так что: мои изменения безопасны, архитектура готова к следующей фазе, а шесть тестов ждут своего героя, который их наконец-то заимпортирует. 😄