Рой — планировщик маршрутов выездных инженеров
Сервис распределяет заявки между бригадами и строит маршруты с учётом временных окон, квалификации, оборудования и транспорта, перепланирует день при появлении новой заявки и объясняет каждое назначение. Задача 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-domv7, клиент 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 (витрина дизайн-системы, переключатель светлой/тёмной темы). Это сквозная проверка связки фронт { 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.