Чат-бот в MAX и подключённое к нему мини-приложение для владельца небольшой сети магазинов одежды (3–15 точек). Хакатон MAX, трек «Эффективный бизнес».
| Бот в MAX | max.ru/t185_hakaton_max_bot |
| Мини-приложение | открывается из чата с ботом, кнопка «Открыть приложение» (адрес https://task-controller.ru) |
| Собственный API | https://task-controller.ru/api — описание в openapi.yaml, проверки в DATA-API.yaml |
| Тестовые данные | backend/testdata/demo_network.json — все точки и люди выдуманы |
Кто пользователь. Владелец сети из нескольких магазинов, который не может каждый день быть в каждой точке, и продавцы-кассиры этих точек.
Проблема. Владелец узнаёт, открылся ли магазин вовремя, приняли ли поставку и закрыли ли кассу, только если сам позвонит или напишет в общий чат. Отчёты в чате теряются, фото приходят без привязки к задаче, а о сбое владелец узнаёт поздно — когда клиент уже увидел закрытую дверь.
Что делает решение.
- Сотрудник отмечает задачи смены прямо в боте: кнопка у каждой задачи и фото-подтверждение. Отметить может только тот, кто по графику сейчас на смене.
- Бот сам напоминает о сроках и сам сообщает владельцу о сбое: задача просрочена или её никто не взял.
- Владелец в мини-приложении ведёт точки, сотрудников, задачи и график смен и видит сводку по всем магазинам за день: проблемные точки сверху.
В штатном режиме владелец не получает ни одного сообщения. Любое уведомление означает, что нужно вмешаться.
Владелец
- Открывает бота, отправляет
/start, нажимает «Я владелец». Бот создаёт сеть и присылает кнопку «Открыть приложение». - В приложении, экран «Точки и сотрудники»: добавляет точку (часы работы, выходные) и сотрудника. Рядом с сотрудником появляется одноразовый код приглашения.
- Экран «Задачи точки»: добавляет задачи — ежедневные или разовые на дату. У задачи есть плановое время, время, с которого её можно отметить, допустимая задержка, нужно ли фото и нужно ли сначала взять её кнопкой «Беру».
- Экран «График смен»: расставляет смены на неделю, нажимает «Проверить покрытие» — приложение показывает часы, когда точка открыта, а на смене никого. Затем публикует неделю.
- Экран «Сводка»: видит все точки за день. Красные — есть просроченная или не взятая задача, жёлтые — задачи сделаны, но с опозданием, зелёные — всё в порядке. Из точки открывается карточка дня: кто и когда отметил каждую задачу, фото, кто сейчас на смене.
Сотрудник
- Открывает бота, нажимает «Я сотрудник» и присылает код от владельца. Бот отвечает: «Готово. Вы подключены к точке …».
- В начале смены бот присылает доску смены: список задач с временем и кнопку «Выполнить» у каждой.
- Перед задачей приходят напоминания: за 15 и за 5 минут до планового времени и за 15 и за 5 минут до срока (плановое время плюс допуск).
- Сотрудник нажимает «Выполнить», бот просит фото именно этой задачи, сотрудник присылает фото. Бот отвечает обновлённой доской, её же получают остальные на смене — чтобы никто не делал работу дважды.
- Если задача требует «Беру», бот спрашивает смену «Кто принимает?». Первый нажавший получает задачу, у остальных кнопка пропадает.
- После смены приходит итог: «Выполнено N из M» и что осталось не отмеченным.
Что получает владелец
- Срок задачи прошёл, отметки нет — сообщение «Точка: задача не отмечена» с кнопкой, которая открывает карточку этой точки на этот день.
- Задачу с «Беру» никто не взял к плановому времени — сообщение «никто не взял».
- Просроченную задачу всё-таки выполнили — отдельное сообщение: когда, кто и на сколько опоздал.
Один аккаунт MAX может быть одновременно владельцем сети и сотрудником своей точки — как владелец небольшого магазина, который сам стоит за прилавком. После привязки по коду он отмечает задачи в боте, а приложение владельца остаётся доступным. Командой /role меняется только то, с чем бот работает в диалоге.
| Компонент | Где | Назначение |
|---|---|---|
| Бот | backend/apps/bot (run_bot) |
Получает события MAX через Long Polling: роли, привязка по коду, отметки, фото, «Беру», «что осталось» |
| Планировщик | backend/apps/bot/scheduler.py (run_scheduler) |
Каждые 30 секунд: доска в начале смены, напоминания, вопросы «Кто принимает?», просрочки, уведомления владельцу, итоги смен |
| Домен и модели | backend/apps/core |
Точки, сотрудники, задачи, смены, отметки; право на отметку по смене; поиск незакрытых часов в графике; статус задачи |
| REST API | backend/apps/api |
API мини-приложения: проверка подписи запуска MAX, доступ только к своей сети |
| Мини-приложение | frontend |
React 19, MAX UI, MAX Bridge; 5 экранов владельца |
| База данных | PostgreSQL 16 (SQLite при локальной разработке без Docker) | Все данные решения |
| Хранилище фото | volume media |
Фото-подтверждения задач |
| https | Caddy (профиль prod) |
Сертификат и проксирование на сервере |
Как связаны части. Бот и планировщик работают с базой через модели Django и с MAX через клиент Bot API. Мини-приложение ходит только в REST API. Django в одном контейнере отдаёт и API (/api/), и собранное мини-приложение (/), поэтому у приложения один https-адрес. Уведомления владельцу содержат ссылку startapp, которая открывает мини-приложение сразу на карточке нужной точки и дня.
Сотрудник ─── MAX ───► бот (run_bot) ──────┐
▲ ▼
└── планировщик ─► PostgreSQL ◄── REST API ◄── мини-приложение ◄── Владелец
(run_scheduler) (Django) (React, MAX Bridge)
cp .env.example .env # Windows: copy .env.example .env
docker compose up --buildПоднимаются db, migrate (применяет миграции и завершается), web, bot, scheduler. Сборка занимает около 1–2 минут.
После запуска:
- http://localhost:8000/api/health/ отвечает
{"status":"ok"}; - http://localhost:8000/ — мини-приложение. Вне MAX оно показывает «Откройте приложение из чата с ботом в MAX»: API пускает только запуски, подписанные MAX;
- без
BOT_TOKENсервисbotзавершается с ошибкойBOT_TOKEN is not setи перезапускается — это ожидаемо, остальное работает.
Важно для проверки. Если запустить локальную копию с тем же
BOT_TOKEN, что и на сервере, два бота начнут перехватывать сообщения друг у друга, и работающий бот в MAX станет терять события. Для проверки в MAX пользуйтесь ботом на сервере, а локальный Docker запускайте безBOT_TOKEN.
Проверить API локально можно токеном проверки (см. «Проверка собственного API»): задайте REVIEW_API_TOKEN в .env, перезапустите и загрузите тестовые данные:
docker compose up -d --build
docker compose exec web python manage.py seed_demo --owner-max-id 900000001
curl -H "Authorization: Bearer <REVIEW_API_TOKEN>" http://localhost:8000/api/dashboard/- Docker с Compose v2; свободный порт 8000 (для профиля
prod— 80 и 443). - Для работы бота — токен бота MAX (выдают организаторы хакатона).
- Для открытия мини-приложения в MAX — публичный https-адрес, который организаторы привязывают к боту. Внутри Docker это не воспроизводится, поэтому для проверки в MAX используется развёрнутый сервер https://task-controller.ru.
Полный список с комментариями — в .env.example.
| Переменная | Назначение |
|---|---|
BOT_TOKEN |
Токен бота (секрет, выдаёт организатор) |
BOT_USERNAME |
Ник бота для ссылки на мини-приложение |
MAX_API_BASE |
Адрес Bot API MAX (https://platform-api2.max.ru) |
MAX_CA_BUNDLE |
Корневой сертификат Минцифры, которым подписан API MAX |
DJANGO_DEBUG, DJANGO_SECRET_KEY |
Режим и секретный ключ Django (при DEBUG=0 ключ обязателен) |
DJANGO_ALLOWED_HOSTS, DJANGO_CSRF_TRUSTED_ORIGINS |
Публичные хосты мини-приложения |
FRAME_ANCESTORS |
Кто может встраивать мини-приложение во фрейм (веб-клиент MAX) |
PUBLIC_BASE_URL, DOMAIN |
Публичный https-адрес и домен для Caddy на сервере |
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT |
Доступ к БД; пустой POSTGRES_HOST включает SQLite |
INIT_DATA_MAX_AGE_SECONDS |
Сколько действует подпись запуска мини-приложения |
REVIEW_API_TOKEN, REVIEW_OWNER_MAX_ID |
Токен автоматической проверки API и MAX id тестового владельца. Пустой токен выключает этот вход |
REMINDER_FIRST_MINUTES_BEFORE, REMINDER_FINAL_MINUTES_BEFORE |
За сколько минут напоминать (15 и 5) |
CLAIM_ESCALATION_MINUTES_BEFORE |
За сколько минут повторить «никто не взял» смене (15) |
PHOTO_WAIT_MINUTES |
Сколько ждать фото, прежде чем задача снова станет открытой (10) |
SHIFT_BOUNDARY_TOLERANCE_MINUTES |
Допуск на границах смены для отметки (5) |
SCHEDULER_INTERVAL_SECONDS, LOG_LEVEL |
Период планировщика (30 с) и уровень логов |
| Порт | Сервис |
|---|---|
| 8000 | web: API, мини-приложение, админка Django (/admin/). Опубликован только на 127.0.0.1 |
| 80, 443 | Caddy, только в профиле prod на сервере |
| 5432 | PostgreSQL, только внутри сети Docker |
- Python 3.13: backend/requirements.txt — Django 5.2 LTS, Django REST framework, httpx, psycopg, WhiteNoise, gunicorn. Версии зафиксированы. Корневой requirements.txt ссылается на него.
- Node 22: frontend/package.json и
package-lock.json— React 19.2.8,@maxhub/max-ui, Vite. Версии зафиксированы. - Все библиотеки с открытыми лицензиями.
- MAX Bot API (
platform-api2.max.ru): получение событий бота (Long Polling), отправка и правка сообщений, ответы на кнопки, загрузка фото. Сертификат API подписан корневым сертификатом Минцифры — он лежит в certs/ и подключается в клиенте бота. - MAX Bridge (
st.max.ru/js/max-web-app.js): подпись запуска мини-приложения (initData), системная кнопка «Назад», параметрstartappдля открытия карточки точки из уведомления.
Других интеграций нет: касса, учётные системы и геолокация в MVP не подключаются.
- Все данные вводит владелец в мини-приложении или сотрудник в боте: точки, сотрудники, задачи, график, отметки, фото.
- Владелец видит только свою сеть: чужая точка, сотрудник или фото отвечают 404. Фото отдаются только через API владельцу этой сети.
- Уволенный сотрудник теряет доступ к отметкам, его будущие смены удаляются, а история отметок остаётся: «Отметил Пётр С.» не пропадает из прошлых дней.
- Итоги прошедших задач не пересчитываются: если после опоздания владелец увеличил допуск, прошлый день всё равно покажет опоздание.
- Из персональных данных хранятся только MAX id, имя из MAX и имя сотрудника, которое ввёл владелец.
Все точки, адреса и люди в тестовых данных выдуманы; к реальному бизнесу они не относятся.
Данные описаны в backend/testdata/demo_network.json: три точки, семь сотрудников и один уволенный, четыре задачи на каждую точку, график на две недели (текущая опубликована, следующая — черновик с одним незакрытым окном, которое находит проверка покрытия). Сегодняшний день первой точки выглядит как типичный проблемный день: открытие с опозданием на 40 минут, подготовка зала вовремя, поставку никто не взял.
Загрузка (повторный запуск безопасен, --reset сначала удаляет прежние демо-данные):
docker compose exec web python manage.py seed_demo --owner-max-id <MAX id владельца>Коды приглашения в файле не хранятся: они одноразовые и создаются заново при загрузке, команда печатает их в консоль. Для проверки в MAX они не нужны — проверяющий создаёт сотрудника сам и получает код в приложении (см. сценарий ниже). Для автоматической проверки API данные загружаются для тестового владельца 900000001.
Проверяющий сам создаёт точку и сотрудника, поэтому готовые коды и демо-данные не нужны: код сотрудника появляется в приложении на шаге 4.
- Два аккаунта MAX — самый наглядный вариант: первый владелец, второй сотрудник.
- Один аккаунт тоже подходит: на шаге 6 переключитесь в «Я сотрудник» и отправьте свой же код. Дальше роль переключать не нужно — приложение владельца открывается кнопкой «Открыть приложение» из чата, а фото и «что осталось» бот принимает. Уведомления владельцу придут в тот же чат.
| # | Действие | Ожидаемый результат |
|---|---|---|
| 1 | Открыть бота, отправить /start, нажать «Я владелец» |
«Вы владелец. Настройки и сводка по точкам — в приложении.» и кнопка «Открыть приложение» |
| 2 | Открыть приложение | Экран «Сводка», пока без точек, с кнопкой «Добавить точку» |
| 3 | «Точки и сотрудники» → «Добавить точку»: название, часы работы, выходные | Точка в списке, под ней часы работы и выходные, например «10–22 · без выходных» |
| 4 | «Добавить сотрудника» в этой точке | Сотрудник со статусом «ждёт привязки» и кодом из 8 символов |
| 5 | Открыть точку → «Задачи точки» → «Добавить разовую задачу на дату»: сегодня, плановое время через 20–30 минут, с фото | Задача в списке; в карточке дня — «Ожидается» |
| 6 | Со второго аккаунта: /start → «Я сотрудник» → отправить код из шага 4. С одного аккаунта: /role → «Я сотрудник» → код |
«Готово. Вы подключены к точке …»; в приложении статус сотрудника меняется на «в боте» |
| 7 | Владелец: «График смен» → смена этому сотруднику на сегодня, которая уже идёт → «Проверить покрытие» → «Опубликовать» | Неделя опубликована; незакрытые часы подсвечены |
| 8 | Подождать до 30 секунд | Сотруднику приходит «Смена началась» со списком задач и кнопкой «Выполнить · ЧЧ:ММ название» |
| 9 | Сотрудник: «Выполнить» → прислать фото | «Фото принято, «…» отмечено в ЧЧ:ММ, вовремя» и обновлённая доска; в карточке дня у задачи — кто, когда и «Посмотреть фото» |
| 10 | Сотрудник: написать «что осталось» | Текущая доска смены |
| 11 | Владелец: добавить ещё одну разовую задачу на сегодня через 2–3 минуты с допуском 0; сотрудник её не отмечает | Когда срок пройдёт, в течение 30 секунд владельцу приходит «Точка: задача «…» не отмечена» с кнопкой в карточку точки; в сводке точка красная |
| 12 | Сотрудник отмечает эту задачу | Владельцу приходит «задача выполнена в …, с опозданием на N минут. Кто отметил: …»; точка в сводке становится жёлтой |
| Ситуация | Что происходит |
|---|---|
| Сотрудник нажимает «Выполнить» в свой выходной | «Сегодня у вас выходной. Ближайшая смена — …» — отметка не принимается |
| Нажал раньше, чем задачу можно отмечать | Бот называет время, с которого можно отметить |
| Два сотрудника одной смены | После отметки одного второй получает обновлённый список, кнопка выполненной задачи пропадает |
| Задача с «Беру» | Смене приходит «Кто принимает?»; первый нажавший получает задачу, у остальных вопрос меняется на «… выполняет Имя»; за 15 минут до планового времени, если никто не взял, — повтор, к плановому времени — сообщение владельцу |
| Нажал «Выполнить», но не прислал фото за 10 минут | Задача снова открыта для отметки |
| Владелец добавил или удалил задачу посреди смены | Смене приходит «Список задач изменился» с новым списком |
| В графике есть часы без людей | «Проверить покрытие» показывает окна по дням; выходные дни точки не считаются окнами |
| Владелец пытается изменить прошедшую задачу | «Итог этой задачи уже записан и не меняется» — история не переписывается |
| API получает неверные данные | 400 и понятный текст в detail, например «Время указано неверно, ожидается ЧЧ:ММ» |
| Запрос к чужой точке | 404 «Точка не найдена» |
- Адрес:
https://task-controller.ru/api. - Контракт: openapi.yaml (OpenAPI 3.1).
- Обязательные проверки: DATA-API.yaml (DATA-API 1.0, проходит валидатор организаторов). Сценарий: сводка, точки, карточка дня, добавление сотрудника и задачи, правка задачи, неверный ввод, график и проверка покрытия. Всё созданное удаляется в
cleanup. - Роль
owner: заголовокAuthorization: Bearer <REVIEW_API_TOKEN>. Токен передаётся организаторам отдельно, в репозитории его нет. Запросы идут от имени тестового владельца с отдельной сетью — реальные данные ему недоступны. - Мини-приложение авторизуется иначе: заголовком
X-Max-Init-Dataс подписанной MAX строкой запуска.
- Бот получает события через Long Polling. События, пришедшие, пока бот перезапускается, могут потеряться. Для промышленного запуска MAX рекомендует Webhook.
- Все точки работают в часовом поясе Europe/Moscow: поле часового пояса есть в модели, но в приложении не редактируется.
- Смена не может переходить через полночь: самое позднее окончание — 24:00 (хранится как 23:59).
- Фото хранятся в volume сервера, без внешнего хранилища и без срока хранения.
- Касса, учётные системы, геопривязка отметки и автоматическое построение графика в MVP не реализованы.
- Мини-приложение рассчитано на владельца. Всё, что делает сотрудник, происходит в боте.
- Демо-данные выдуманы (см. «Тестовые данные»).
docker compose down # остановить, данные БД и фото сохраняются
docker compose up -d # запустить снова
docker compose down -v # остановить и удалить данные БД и фотоНужны сервер с Ubuntu 24.04 (от 2 ГБ RAM), открытые порты 22, 80 и 443 и домен, A-запись которого указывает на IP сервера. Сертификат получает и продлевает Caddy.
git clone https://github.com/Shipovmax/hackathon_max.git
cd hackathon_max
bash deploy/server-setup.sh # Docker, Compose и swap; после него перезайти по SSH
cp .env.example .env && nano .envЗначения для сервера:
BOT_TOKEN=<токен бота>
DJANGO_DEBUG=0
DJANGO_SECRET_KEY=<python3 -c "import secrets; print(secrets.token_urlsafe(50))">
DJANGO_ALLOWED_HOSTS=<домен>,localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=https://<домен>
DOMAIN=<домен>
PUBLIC_BASE_URL=https://<домен>
POSTGRES_PASSWORD=<длинный случайный пароль>
REVIEW_API_TOKEN=<python3 -c "import secrets; print(secrets.token_urlsafe(32))">
docker compose --profile prod up -d --build
curl https://<домен>/api/health/ # {"status":"ok"}
docker compose logs -f bot # в логе: polling as @...Обновление: git pull && docker compose --profile prod up -d --build (миграции применяет сервис migrate). Смена домена: bash deploy/switch-domain.sh <домен>. Если Docker Hub недоступен, добавьте зеркало {"registry-mirrors": ["https://mirror.gcr.io"]} в /etc/docker/daemon.json и перезапустите Docker.
backend/ Django: apps/core (модели, домен), apps/api (REST), apps/bot (бот, планировщик)
backend/testdata/ тестовые данные (JSON)
frontend/ мини-приложение: React + MAX UI + MAX Bridge
certs/ корневой сертификат для API MAX
deploy/ Caddyfile (https), настройка сервера, смена домена
openapi.yaml контракт собственного API
DATA-API.yaml обязательные проверки API для платформы оценки
Dockerfile, compose.yaml, .env.example, .dockerignore