Рой — планировщик маршрутов выездных инженеров

Сервис распределяет заявки между бригадами и строит маршруты с учётом временных окон, квалификации, оборудования и транспорта, перепланирует день при появлении новой заявки и объясняет каждое назначение. Задача 3 хакатона МИК-2026, «Билайн Бизнес».

Монорепозиторий под управлением pnpm workspaces: бэкенд-API, фронтенд и решатель.

Что где почитать

Документ О чём
docs/jury.md запуск в Docker одной командой
docs/solver.md алгоритм решателя по шагам: от задачи в очереди до нарядов
docs/assumptions.md допущения и решения с ценой; где датасет расходится с ТЗ
docs/dataset.md тестовый набор: объём, единицы измерения, нормативы
docs/tools.md ПО, лицензии и как применялись ИИ-инструменты
CLAUDE.md канонический документ для AI-агентов: карта кода, команды, «гочи»
docs/plans/ рабочие планы с замерами — как принимались решения

Соответствие заданию

Пункт ТЗ Где реализовано Чем проверяется
1. Распределение по квалификации и доступности solver/src/services/solver.py — домен VehicleVar сужается по навыкам и оборудованию check:plan, check_solver_policy.py
2. Временные окна, транспорт, оборудование там же: окно ограничивает прибытие; матрица своя на каждый транспорт check:plan — проверяет окна, смены, навыки и оборудование каждого наряда
3. Минимизация времени и нарушений SLA целевая функция: цена вывода бригады, штрафы за отброс по приоритету, аварии раньше KPI /plans/day, смоук d8
4. Автоматическое перепланирование при новой заявке routes/orders/replan-trigger.ts + ветка replan решателя смоук replan-trigger.sh
5. Карта и причины выбора маршрута apps/admin — карта маршрутов, карточка визита с факторами и отклонёнными кандидатами (routes/plans/explanation.ts) браузерный прогон; e2e на это есть, но сьют сейчас не загружается (см. docs/assumptions.md)
6. Дифф после перепланирования entities/plan/lib/diff.ts + плашка над таймлайном «Плана дня» юнит-тесты diff.test.ts, браузерный прогон на задержке
7. Выгрузка результата (CSV / JSON) GET /plans/day/export (routes/plans/export.ts) + кнопка «Выгрузить» в шапке плана смоук export.sh, юнит-тесты export.test.ts

Для жюри / быстрый запуск в Docker: docs/jury.md.

Для AI-агентов: канонический подробный документ по проекту — CLAUDE.md (стек, слои, команды, конвенции, «гочи»). Агентские скиллы/команды/субагенты — в .claude/ (см. .claude/SUMMARY.md).

Стек

  • Пакетный менеджер: pnpm (workspaces), Node >= 24
  • Бекенд (apps/api): Hono + @hono/zod-openapi, аутентификация — better-auth, логирование — pino, Scalar-доки
  • БД (packages/db): PostgreSQL + Drizzle ORM (+ drizzle-kit для миграций)
  • Фронтенд (apps/admin): Vite + React 19 + TypeScript, TailwindCSS v4, Zustand (клиентское состояние), TanStack Query (серверное), react-router-dom v7, клиент better-auth. Архитектура — Feature-Sliced Design
  • Дизайн-система (packages/ui): React-компоненты на семантических токенах; токены/тема — packages/shared/theme.css (Tailwind v4, тёмная тема по классу .dark, шрифт Onest)
  • Общий код (packages/shared): типизированный Hono-клиент (hc), конфиг better-auth, типы ресурсов, theme.css
  • Решатель (solver/): Python 3.12 + Google OR-Tools — VRPTW-оптимизатор. Не HTTP-сервис: слушает канал PostgreSQL, пишет наряды в ту же базу. Управляется uv
  • Маршрутизация (Valhalla 3.5.1, Docker): матрицы времени и расстояния по дорогам ЦФО, изохроны зоны досягаемости. Поднимается локально, данные наружу не уходят

Структура

.
├── apps/
│   ├── api/      # Бекенд на Hono: округа, районы, заявки, инженеры, планы + auth
│   └── admin/    # Админка (Vite + React SPA), структура по FSD:
│                 #   app / pages / features / entities / shared
├── packages/
│   ├── db/       # Слой БД: схемы Drizzle, миграции, подключение к Postgres
│   ├── shared/   # @beeline/shared — hc-клиент, конфиг auth, типы, theme.css
│   ├── ui/       # @beeline/ui — React-компоненты дизайн-системы
│   └── onboarding/ # @beeline/onboarding — движок туров driver.js (router-агностичный)
├── docker-compose.yml   # Локальный PostgreSQL
├── .env.example         # Шаблон общего окружения для api + db
├── CLAUDE.md            # Подробный док проекта для AI-агентов
└── pnpm-workspace.yaml

Фронтенд задуман как два независимых приложения: apps/admin (админ-панель) и будущее клиентское. Сейчас реализуется только admin; packages/ui и packages/shared — переиспользуемый кросс-аппный слой.

Требования

  • Node.js >= 24 (см. devEngines в package.json)
  • pnpm >= 11
  • Docker (для локального PostgreSQL) — либо свой инстанс Postgres

Быстрый старт

pnpm install                                 # 1. зависимости всех воркспейсов
cp .env.example .env                         # 2. общий .env для apps/api + packages/db
cp apps/admin/.env.example apps/admin/.env   # 3. .env админки (VITE_API_URL)
docker compose up -d db valhalla             # 4. PostgreSQL и маршрутизатор
pnpm build                                   # 5. @beeline/db и @beeline/api собираются в dist
pnpm db:migrate                              # 6. миграции (db:generate — создать новую)
pnpm --filter @beeline/api seed:tz           # 7. датасет заказчика на 2026-08-17
pnpm --filter @beeline/api seed:admin        # 8. админ из ADMIN_EMAIL/ADMIN_PASSWORD
pnpm dev:api                                 # 9. API на http://localhost:3030 (tsx watch)
pnpm dev:admin                               # 10. админка на http://localhost:5173 (Vite)

Отдельным терминалом — воркер решателя, без него задачи расчёта висят в queued и «План дня» остаётся пустым:

cd solver && uv sync --frozen
# httpx уважает системный прокси и не достучится до localhost — гасим его для процесса
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy NO_PROXY='*' uv run python src/main.py

Порядок сидов важен. seed:tz начинается с TRUNCATE … user CASCADE, то есть сносит созданного админа — если запустить его после seed:admin, логин отдаст 401. Старый seed:data не запускается: он написан под мёртвую схему с городами и заказчиками.

Дата стенда — 2026-08-17. Датасет организаторов лежит на одном дне, поэтому VITE_DEFAULT_PLAN_DATE и VITE_DEFAULT_DISTRICT в apps/admin/.env задают предвыбранные день и округ: по «сегодня» план, календарь и список инженеров были бы пусты.

Valhalla нужны готовые тайлы ЦФО в ./valhalla_tiles (~888 МБ, в git их нет — передаются отдельно). Без них матрицы не построятся и расчёт завершится ошибкой.

API-доки (Swagger/Scalar) — http://localhost:3030/reference (нужен запущенный pnpm dev:api).

  • .env — общий файл для apps/api и packages/db (оба резолвят его из корня). Секрет: BETTER_AUTH_SECRET (>= 16 символов), например openssl rand -base64 32.
  • Порт Postgres берётся из POSTGRES_PORT в .env (на этой машине 5432 занят другим проектом — используется 5433; проверяйте docker ps перед поднятием ещё одной БД).
  • Для админки есть свой apps/admin/.env (только VITE_API_URL); шаблон — apps/admin/.env.example.
  • Origin админки (http://localhost:5173) уже прописан в ALLOWED_ORIGINS.

Памятка по .env и портам

Два разных .env (оба в .gitignore, из шаблонов — копировать один раз после клона):

Файл Кто читает Ключевые переменные Шаблон
.env (корень) apps/api, packages/db DATABASE_URL, POSTGRES_PORT, BETTER_AUTH_SECRET, BETTER_AUTH_URL, ALLOWED_ORIGINS, ADMIN_EMAIL/ADMIN_PASSWORD .env.example
apps/admin/.env apps/admin (Vite) VITE_API_URL apps/admin/.env.example
cp .env.example .env                          # корневой (api + db)
cp apps/admin/.env.example apps/admin/.env    # админка (VITE_API_URL)

Порты по умолчанию: API — 3030, админка (Vite) — 5173, Postgres — 5433 (не 5432).

Частые проблемы

  • EADDRINUSE ... port: 3030 при pnpm dev:api — порт занят, обычно второй экземпляр API (tsx watch не умер после закрытого терминала). Найти и прибить процесс:

    Get-NetTCPConnection -LocalPort 3030 -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }

    Затем снова pnpm dev:api.

  • Запрос авторизации уходит на 5173 вместо 3030 (http://localhost:5173/auth/get-session) — нет apps/admin/.env или в нём пустой VITE_API_URL. Без переменной better-auth берёт origin страницы (5173). Лечится cp apps/admin/.env.example apps/admin/.env + перезапуск Vite (import.meta.env.VITE_* вшивается на старте, на лету не подхватывается).

  • CORS-ошибки на все запросы к API — Vite молча уехал с занятого 5173 на 5174/5175 (смотрите порт в баннере Vite). Новый origin не в ALLOWED_ORIGINS. Освободите 5173 либо добавьте порт в ALLOWED_ORIGINS (через запятую) в корневом .env и перезапустите API (tsx watch не перечитывает .env).

  • Сменили порт API — синхронно поправьте VITE_API_URL (apps/admin/.env), BETTER_AUTH_URL и ALLOWED_ORIGINS (корневой .env), иначе фронт/CORS отвалятся.

Как проверить

Бекенд:

  • API-доки (Swagger/Scalar): http://localhost:3030/reference — интерактивный UI по всем эндпоинтам, «попробовать», + отдельный источник по auth. Требуется поднятый API (pnpm dev:api).
  • http://localhost:3030/doc — сырой OpenAPI-спек (JSON), из него рисуется /reference
  • http://localhost:3030/ — корневой эндпоинт → { "status": "ok", "data": { "message": "Test API" } }
# на машинах с системным прокси localhost перехватывается — нужен --noproxy
curl --noproxy '*' http://localhost:3030/districts
curl --noproxy '*' "http://localhost:3030/plans/day?districtSlug=vostok&date=2026-08-17"

Проверки, которые должны быть зелёными:

pnpm --filter @beeline/api check:plan 2026-08-17   # инварианты готового плана
pnpm --filter @beeline/api exec vitest run         # юнит-тесты API (без watch)
cd solver && uv run python src/check_solver_policy.py   # политика решателя без БД
bash apps/api/scripts/smoke/replan-trigger.sh      # заявка сама попадает в план
bash apps/api/scripts/smoke/export.sh              # выгрузка: BOM, колонки, число строк
pnpm --filter admin test                           # юнит-тесты фронта (дифф плана, кластеры карты)

Админка: http://localhost:5173 — план дня, календарь, заявки, инженеры, рабочий график, справочники (/catalogs → типы работ, навыки, оборудование, типы графиков, округа и районы, матрица совместимости — везде поиск/фильтры, создание и правка) и /ui-kit (витрина дизайн-системы, переключатель светлой/тёмной темы). Это сквозная проверка связки фронт ↔ бек (CORS + конверт { status, data, meta }).

Полезные скрипты (из корня)

Команда Что делает
pnpm dev:api Запуск API в dev-режиме
pnpm dev:admin Запуск админки в dev-режиме
pnpm db:migrate Применить миграции к БД
pnpm db:generate Сгенерировать миграцию из схем Drizzle
pnpm build Собрать все пакеты (pnpm -r build)
pnpm format Прогнать Prettier по репозиторию

Фронтенд-архитектура (FSD)

apps/admin следует Feature-Sliced Design. Слои сверху вниз: app → pages → widgets → features → entities → shared. Импорты — только вниз; слайсы одного слоя изолированы; публичный API слайса — через index.ts.

apps/admin/src/
  app/        шелл, провайдеры, роутинг (react-router-dom), стили
  pages/      plan, calendar, orders, engineers, catalogs, ui-kit — цели маршрутов
  features/   pick-district, create-order, assign-order, theme-toggle — по одному действию
  entities/   district — чтение (useDistricts) + ключи кэша + типы
  shared/     api (client+auth), config (query-client), model (ui-store)

Роутинг живёт только в app (app/routing.tsx + app/root-layout.tsx). Sidebar из @beeline/ui остаётся router-агностичным. Подробности и правила — в CLAUDE.md.

Дизайн-система (@beeline/ui + theme.css)

  • Токены — packages/shared/theme.css (Tailwind v4 @theme), тёмная тема по классу .dark, шрифт Onest. Приложение импортит @import '@beeline/shared/theme.css' и добавляет @source для packages/ui/src + packages/shared/src.
  • Компоненты — packages/ui (@beeline/ui). В разметке только семантические утилиты (bg-canvas/bg-surface/text-fg/border-border/bg-brand), без примитивов и произвольных значений. Иконки — в одном месте (packages/ui/src/icons/).

Общий код: @beeline/shared

Source-only TS-пакет (приложения импортируют исходники напрямую):

  • Типизированный RPC-клиент Hono (hc) — createApiClient(baseUrl); типы путей/query/тела/ответов выведены из исходников бэкенда:
    import { createApiClient, unwrap } from '@beeline/shared';
    const api = createApiClient(import.meta.env.VITE_API_URL);
    const { data, meta } = await unwrap(
      await api.districts.$get({ query: { limit: '10' } }),
    );
    Тип роутера отдаётся через apps/api → api/hc. unwrap() разворачивает конверт { status, data, meta? }, сужает до успешной ветки и бросает ApiError на не-2xx.
  • Типы ресурсов (District, DistrictsQuery, Order) выводятся из клиента — руками не дублируются.
  • better-auth — authClientConfig(baseURL) даёт базовый конфиг; плагины добавляет каждое приложение.

⚠️ Типы hc берутся из собранных деклараций apps/api (api/dist/hc.d.ts):

  • перед pnpm --filter admin build / typecheck соберите API: pnpm --filter api build (в pnpm build учтено — топологический порядок);
  • import type стирается, поэтому pnpm dev:admin работает без сборки API;
  • после изменения роутов — пересоберите типы: pnpm --filter api build.

Документация для агентов

  • CLAUDE.md — единый подробный док проекта (стек, слои, команды, конвенции, гочи). Держится в актуальном состоянии и обновляется в том же коммите, что и изменение структуры/команд/контрактов (см. правило в CLAUDE.md; pre-commit-хук напоминает).
  • .claude/ — субагенты (agents/), скиллы (skills/), slash-команды (commands/); индекс — .claude/SUMMARY.md.

Деплой

CI/CD в .gitlab-ci.yml (ветка main): rsync кода, pnpm install, сборка @beeline/db и api, миграции, перезапуск через PM2. Продакшен-.env — из переменной $PRODUCTION_ENV GitLab. SPA-фронт при статик-хостинге требует history-fallback на index.html.