Команда — шесть разработчиков, полтора десятка параллельных проектов. Задачи жили в Excel, чатах и головах. В какой‑то момент стало понятно, что «а кто это делает?» задаётся чаще, чем «как это сделать», — пора заводить трекер.

Дальше случилось то, что случалось у многих: мы честно прошлись по рынку, везде что‑то мешало, и в понедельник я открыл пустой репозиторий. Во вторник команда уже вела задачи в своём трекере.

А самое интересное произошло между этими точками: канбан‑доска — то, ради чего всё затевалось, — прожила в интерфейсе два с половиной часа. По истории коммитов видно точно.

Расскажу по порядку: почему не подошло готовое, что выкинули, что оставили и почему «свой трекер за два дня» — это не подвиг, а холодный расчёт про длину петли обратной связи.

Почему не готовое

Сразу дисклеймер: всё перечисленное ниже — нормальные инструменты. Не подошли они нам из‑за наших ограничений, а не потому что плохие.

Jira и Linear отпали первыми: данные о внутренних проектах не должны жить во внешнем облаке, а с обслуживанием российских аккаунтов у обоих вендоров всё сложно. Kaiten, YouGile, Weeek — приличные российские альтернативы, но это опять облако с чужим хранением, а по деньгам на команду — подписка за то, чем мы будем пользоваться на пять процентов.

Self‑hosted Plane был ближе всего, но тащить и обслуживать чужой комбайн ради шести человек не хотелось.

GitHub Issues мы используем и любим, но тут споткнулись о главное: трекер нужен не только разработчикам. Руководителю нужны карточки, сроки и картинка загрузки — а не list view с лейблами. Фраза, убившая этот вариант, звучала так: «Issues не умеют пользоваться менеджеры». Это не претензия к менеджерам — это факт о интерфейсе.

И тут важный момент честности. Решающим аргументом за «написать своё» была не экономия и не уникальные требования. Нам хотелось это написать. Если у вас нет такого пункта в мотивации — берите Kaiten и живите спокойно. Но если есть, дальше будет интересно, потому что бюджет эксперимента — два дня.

Все инструменты нормальные. Не подошли ограничения, а не качество
Все инструменты нормальные. Не подошли ограничения, а не качество

Стек: скучный, и это осознанно

FastAPI + SQLAlchemy 2 + Alembic на бэкенде, Next.js 15 + React 19 + TanStack Query на фронте, PostgreSQL 16, Redis, nginx. Шесть контейнеров docker compose.

Свободного сервера не было — развернули на виртуалке, где уже жил другой наш сервис. Правила совместного проживания простые: отдельный compose‑проект со своими томами и сетью, наружу — один‑единственный порт 8443, системный nginx соседа не трогаем. Живут вместе, друг о друге не знают.

Шесть контейнеров. Worker — тот же образ, что api, только вместо HTTP он разгребает очередь уведомлений
Шесть контейнеров. Worker — тот же образ, что api, только вместо HTTP он разгребает очередь уведомлений

Утро первого дня: канбан. Обед первого дня: похороны канбана

Первый коммит — классика жанра: доска, колонки, карточки, drag‑and‑drop. Всё как у больших, dnd‑kit подключён, карточки летают.

Коммит через два с половиной часа называется «Отказ от досок».

Что произошло: мы начали переносить в трекер реальные задачи и смотреть, как их видят реальные люди. И выяснилось, что доска отвечает на вопрос, который у нас никто не задаёт. «Что в какой стадии по проекту X» — это вопрос компании, где над одним продуктом работает десять человек. У нас другая структура: шесть человек, у каждого по два‑три проекта. Вопросы звучат иначе:

  • у разработчика: «что моё и что горит?»

  • у ответственного за проект: «что я поставил и сколько оно уже висит?»

  • у руководителя: «кто перегружен и какие проекты стоят?»

Ни один из этих вопросов не отвечается доской. Все три отвечаются списком с фильтрами и отметками времени.

Доску выкинули из интерфейса целиком. Вместо неё: плитки проектов на главной, внутри проекта — список задач со стадиями, отдельно — сводный список по всем проектам и дашборд загрузки.

А вот из модели данных канбан не ушёл — и это, пожалуй, главный архитектурный вывод этих двух дней. Колонки остались в базе как этапы жизненного цикла с машиночитаемым признаком:

Project → Board → BoardColumn(kind: поставлено | в работе | на проверке | готово) → Task

Стадия задачи — это её колонка. Кнопки интерфейса смотрят на kind, а не на название, поэтому колонки можно переименовывать, не ломая логику. Канбан оказался моделью данных, а не интерфейсом: этапы и переходы — ценность, визуализация столбиками — вкусовщина, которую каждая команда готовит по‑своему.

Жизненный цикл вместо перетаскивания

Вместо драг‑энд‑дропа — четыре кнопки с правами:

  Четыре кнопки, три отметки времени. Метрика "сколько висит" получается бесплатно
Четыре кнопки, три отметки времени. Метрика «сколько висит» получается бесплатно

«Принять» и «Завершить» может только исполнитель. «Принять работу» и «Вернуть» — только ответственный за проект. Каждый переход пишет отметку времени: started_at, submitted_at, completed_at.

Из этих трёх полей интерфейс считает то, ради чего всё затевалось: сколько задача провисела до того, как её взяли, и сколько была в работе. Это единственная метрика, которую у нас реально спрашивают. Не burndown, не velocity — «почему это висит третью неделю».

Две детали про роли, которых нам не хватало в готовых трекерах:

  • Ответственность — свойство проекта, а не человека. Один и тот же сотрудник ведёт один проект и рядовой исполнитель в двух других. В моделях это один флажок ProjectMember.is_lead, в жизни — отсутствие вечного вопроса «а кто тут главный».

  • Замещение на отпуск. Ответственный назначает временного заместителя на период: внутри периода у того все права ведущего, после — всё возвращается само. Ни в одном лёгком трекере из нашего списка такого нет, а в жизни отпуска случаются чаще, чем миграции на новый трекер.

Второй день: петля обратной связи длиной в вечер

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

Между «нам неудобно» и «поправлено» прошло два часа. И вот тут — главный аргумент всей истории.

У вендора этот путь выглядит так: тикет в поддержку → «спасибо, передали команде» → роадмап → может быть, через квартал. У self‑hosted опенсорса: issue → обсуждение → мейнтейнер занят → форкать самим? В своём инструменте: услышал за завтраком → закоммитил к обеду.

Свой трекер — это не про «мы напишем лучше Atlassian». Не напишем. Это про длину петли обратной связи: часы вместо кварталов. Для инструмента, которым команда пользуется каждый день, короткая петля перевешивает длинный список фич, которыми не пользуется никто.

Интеграция с GitHub: вебхуки не нужны

Задачи задачами, но код живёт в GitHub, и связка «задача ↔ pull request» была обязательным требованием. Каноничный путь — вебхуки: GitHub стучится к вам при каждом событии. Для этого нужен белый адрес и доверенный TLS‑сертификат. У нашей виртуалки нет ни того ни другого — и заводить их ради трекера не хотелось.

Решение скучное и оттого прекрасное: опрос. Раз в пять минут сервер сам ходит в GitHub API и забирает состояние pull request'ов. Входящих соединений — ноль, сертификат не нужен, плата — задержка в пять минут, которую никто ни разу не заметил. Есть кнопка «Обновить сейчас» для нетерпеливых.

Что происходит на каждом проходе:

1. PR связывается с задачей разработчика по имени ветки. Трекер в карточке задачи подсказывает имя ветки вида KEY-12-... — если разработчик так её и назвал, связь возникает сама:

def tasknumber_from_branch(project_key: str, branch: str) -> int | None:

    """Из имени ветки достаёт номер задачи: KEY-12-opisanie → 12."""

    match = re.match(rf"^{re.escape(project_key)}-(\d+)(?:-|$)", branch, re.IGNORECASE)

    return int(match.group(1)) if match else None

2. На каждый новый PR автоматически заводится задача «Проверить PR #N» ответственному за проект — с высоким приоритетом и уведомлением. Ревью перестало быть тем, о чём нужно помнить: оно приходит как обычная задача.

3. Состояние PR (открыт / влит / закрыт) обновляется в карточке само.

Отдельно про токены. У каждого проекта — свой fine‑grained токен с правами read‑only на конкретный репозиторий; общего «токена системы» нет в принципе. Хранится он в базе зашифрованным, причём без отдельного секрета — ключ шифрования выводится из уже существующего SECRET_KEY:

def _cipher() -> Fernet:

    digest = hashlib.sha256(get_settings().secret_key.encode()).digest()

    return Fernet(base64.urlsafe_b64encode(digest))

def mask(value: str) -> str:

    """Показ токена в интерфейсе: подтверждает наличие, не раскрывая сам токен."""

    if len(value) <= 8:

        return "••••"

    return f"{value[:4]}…{value[-4:]}"

Логика простая: у базы есть бэкапы, у бэкапов — своя жизнь, и токены в открытом виде в них лежать не должны. А заводить второй секрет ради шифрования первого — путь в бесконечность. Обратно токен интерфейс не отдаёт никогда — только первые и последние четыре символа.

Все стрелки наружу. Внутрь не ходит никто — поэтому не нужны ни белый IP, ни сертификат
Все стрелки наружу. Внутрь не ходит никто — поэтому не нужны ни белый IP, ни сертификат

Скучная надёжность, которая окупилась сразу

Три решения, принятые в первый день «на всякий случай»:

Уведомления — через таблицу и воркер. Интерфейс никогда не отправляет письмо или сообщение в Telegram сам — он кладёт запись в таблицу, а отдельный воркер разгребает очередь с тремя попытками. Когда SMTP однажды задумается, никто этого не заметит: задачи ставятся, кнопки нажимаются, письма догонят.

Время — только UTC. Все отметки — timestamptz, перевод в местное время — в одном‑единственном модуле фронтенда. Правило звучит занудно ровно до первого «почему задача создана завтра».

История изменений — с первого дня. Каждое действие пишется в ActivityLog. Не для аудита в бюрократическом смысле — для ответа на вопрос «кто и когда перевесил задачу», который возникает на третий день жизни любого трекера.

Плюс гигиена по мелочи: пароли — Argon2, сессия — httpOnly‑cookie, саморегистрации нет (учётки заводит администратор — для внутреннего инструмента это фича, а не недостаток).

Чего нет — и почему пока не больно

Честный список из README, раздел «Что пока не сделано»: вложений к задачам нет (модель заложена, интерфейса нет), смены пароля самим сотрудником нет, напоминаний о сроке нет.

И — да: тестов нет. Знаю. Для сервиса на шесть доверенных пользователей во внутренней сети это осознанный размен: цена ошибки — «перезагрузили контейнер», а не «потеряли деньги клиента». Правило, которым я это для себя оформил: тесты появляются вместе с первым пользователем, которому нельзя сказать «ой, сейчас поправлю». Если трекер переживёт квартал — появятся.

Как это писалось: два дня и девять коммитов

Весь трекер — девять коммитов, между первым и последним — около 28 часов календарного времени.

История честнее любых рассказов: «Отказ от досок» — через 2 часа 26 минут после первого коммита
История честнее любых рассказов: «Отказ от досок» — через 2 часа 26 минут после первого коммита

Что в итоге

Трекер в бою с первого дня, команда внутри, Excel с задачами торжественно забыт. Из хотелок — напоминания о сроках и вложения; из планов — прикрутить бота, который по истории ActivityLog будет раз в неделю писать сводку «что зависло и у кого».

Если вычесть эмоции, история раскладывается в три пункта:

  1. Считайте не стоимость подписки, а стоимость несделанных фич. Замещение ответственного на отпуск и автозадача на каждый PR — вещи, которые нам нужны каждую неделю и которых нет ни в одном готовом лёгком трекере. Не потому что вендоры глупые — потому что это фичи ровно нашей структуры команды.

  2. Канбан — это модель данных. Этапы, переходы, отметки времени — берите. Доску с колонками — только если у вас правда есть вопрос «что в какой стадии», а не потому что так выглядят все трекеры.

  3. Свой инструмент оправдывается одной метрикой — длиной петли обратной связи. Если «нам неудобно» превращается в «поправлено» за часы, инструмент живёт. Если у вас нет ресурса держать эту петлю короткой — не начинайте, возьмите готовое.

Ну и главное. Мы два дня писали то, что можно было купить за пару тысяч рублей в месяц. Жалеем ли? Судя по тому, что руководитель теперь сам смотрит задачи и дашборд загрузки вместо того, чтобы спрашивать в чате «а кто это делает», — ни секунды.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Где живут задачи вашей команды?
52.94%Jira или облачный трекер9
11.76%GitHub/GitLab Issues2
11.76%Самописный инструмент2
0%Excel, чаты и головы0
23.53%У нас канбан-доска, и она живая4
Проголосовали 17 пользователей. Воздержались 8 пользователей.