Минимальный веб-агрегатор доступности бензина на АЗС. Пользователь вводит город, район или регион России, а приложение показывает сводку по территории и список станций. Сервер запрашивает источники сам, поэтому браузер не упирается в CORS.
Интерфейс содержит сводку, интерактивную карту АЗС с кластеризацией, таблицу и мобильные карточки, строку поиска и мультивыбор видов топлива и статусов через выпадающие списки с галочками. Карта при разрешении браузера открывается около местоположения пользователя и после остановки перемещения загружает станции для видимой области с запасом. Малые сдвиги внутри загруженной рамки не создают новых запросов; при дальнейшем перемещении догружаются только открывшиеся полосы, а прежние маркеры сохраняются до выхода за более широкую буферную область. При далёком масштабе нужно приблизить карту. Цвет маркера показывает вероятностный статус, а карточка на карте — объединённые источники, согласованность сигналов, топливо, цены и свежесть. Карта и список используют одни фильтры, но таблица остаётся результатом текстового поиска. Viewport-запрос потоково отдаёт накопленные АЗС после ответа каждого источника, поэтому маркеры появляются постепенно до завершения всей догрузки. Ожидание каждого источника ограничено 12 секундами; последовательное обогащение ценами Яндекса не запускается. Полный поиск территории ожидает каждый основной источник не более 8 секунд, медленный браузерный Sber — не более 2 секунд, а ценовую проверку Яндекса — не более 4 секунд. Успешная частичная сводка кратковременно кэшируется вместе с предупреждениями, поэтому недоступный источник не заставляет повторный поиск снова ждать полный тайм-аут. Браузер прекращает ожидание карты через 18 секунд и показывает повторяемую ошибку вместо бесконечного индикатора. Таблица поддерживает сортировку по каждой колонке, пагинацию по 10/25/50/100 строк, липкую шапку и вертикальную прокрутку; колонки помещаются по ширине и перетаскиваются мышью. По умолчанию показываются 25 строк. Фильтры и поиск возвращают пользователя на первую страницу. Размер окна таблицы изменяется за правый нижний угол. Размер, прокрутка, порядок колонок, город, поиск, фильтры, сортировка, страница и количество строк сохраняются в браузере и восстанавливаются после F5; данные повторно берутся из серверного кэша. При переходе в таблицу загрузка скрытой карты приостанавливается. Возврат на карту восстанавливает найденную территорию и заново загружает только актуальную видимую область, не используя запрос со старыми границами скрытой панели. Если в найденной территории нет АЗС, карта всё равно переходит к её bbox, а не оставляет на экране предыдущий район. Кнопка «Обновить весь кэш» сбрасывает данные источников и заново собирает текущую территорию.
npm startОткройте http://localhost:3000.
Скопируйте .env.example в .env, задайте TELEGRAM_BOT_TOKEN и включите
polling:
TELEGRAM_POLLING_ENABLED=true
TELEGRAM_BOT_TOKEN=123456789:real-token-goes-hereПосле npm start бот принимает /start, /help и название города или региона.
Поиск проходит через ту же серверную функцию и те же источники, что и веб-версия;
бот возвращает компактную сводку, а веб — полную таблицу и фильтры. .env не
попадает в git. Статус polling доступен в GET /api/health без раскрытия токена.
При старте бот обновляет описание профиля и меню команд. В /help приведены
примеры поиска города, района города, района области и населённого пункта с
уточнением региона.
Сайт и ответы бота показывают короткий хеш и дату текущего коммита. При локальном
запуске метаданные читаются из Git. Docker-образ не копирует историю репозитория:
передайте GIT_COMMIT_SHA и GIT_COMMIT_DATE как build args (Compose читает их
из одноимённых переменных окружения). Если они не заданы, интерфейс корректно
скрывает неизвестные Git-метаданные.
Каждый новый коммит автоматически увеличивает patch-версию приложения через
отслеживаемый .githooks/pre-commit и scripts/bump-version.js. Один раз после
клонирования включите проектные hooks командой git config core.hooksPath .githooks.
Повторный запуск неудавшегося коммита не увеличивает уже подготовленную версию
ещё раз. Текущая версия синхронно хранится в package.json и package-lock.json
и показывается вместе с хешем.
Сайт и Telegram-бот записывают обезличенные события в PostgreSQL, если заданы
DATABASE_URL и ANALYTICS_HASH_SALT. Сырые IP-адреса, Telegram ID, имена и
тексты поиска не сохраняются. Недоступность базы статистики не блокирует поиск
АЗС или ответы бота.
DATABASE_URL=postgresql://user:password@host:5432/database
DATABASE_SSL=0
# Только для сервера с заведомо самоподписанным сертификатом:
DATABASE_SSL_INSECURE=0
ANALYTICS_HASH_SALT=replace-with-a-long-random-secret
STATS_ADMIN_TOKEN=replace-with-a-long-random-admin-tokenПосле запуска откройте /admin.html и введите STATS_ADMIN_TOKEN. Сводка за
30 дней показывает просмотры и уникальных посетителей сайта, поиски, события
веб-роботов, сообщения и уникальных пользователей Telegram-бота. Таблица и
индексы с префиксом benz_analytics_ создаются автоматически при подключении.
Образ включает Node.js 22 и Chromium для Sber browser-worker. Секреты в образ
не копируются. Compose читает локальный .env, но передаёт контейнеру только
явно разрешённые переменные Benz AI; посторонние значения из файла не попадают
в runtime.
$env:GIT_COMMIT_SHA = (git rev-parse HEAD)
$env:GIT_COMMIT_DATE = (git show -s --format=%cI HEAD)
docker compose build
docker compose up -d
docker compose ps
Invoke-RestMethod -Uri http://127.0.0.1:3000/api/healthЕсли внешний порт 3000 занят, задайте BENZ_AI_HOST_PORT только для
Compose-команды или в локальном .env; внутри контейнера приложение всегда
слушает 3000. Пока используется Telegram long polling, запускайте только одну
реплику приложения: несколько реплик с одним bot token будут конкурировать за
updates. Остановка:
docker compose downCompose не создаёт тома и не удаляет внешние Docker-ресурсы. Временный профиль Chromium находится в container tmpfs и исчезает при остановке контейнера.
- T-Bank Fuel — активный источник:
GET /api/v1/stationsс границами карты. - Alfa AZS — активный источник вероятностного наличия и опубликованных цен.
Endpoint возвращает общероссийский снимок, поэтому приложение кэширует его на
60 секунд и самостоятельно оставляет точки внутри найденной территории.
Защитная HTTP-проверка завершается лёгкой серверной cookie-сессией без запуска
дополнительного Chromium. Установите
ENABLE_ALFA_AZS=0, чтобы полностью отключить запросы к источнику. - BenzUp — лицензируемый источник каталога АЗС и цен. Включается через
BENZUP_API_TOKEN; адрес API по умолчанию —https://api.omt-consult.ru/v2/stations. - Multigo — активный источник ближайших объектов категории АЗС:
POST https://multigo.ru/api/9/near/listс JSON-полямиlat,lngиlimit. Он дополняет каталог, но его общий статус не трактуется как наличие топлива. В выдачу попадают только точки внутри найденной территории; чистые ЭЗС отбрасываются. Поскольку передаваемый API идентификатор может меняться между запросами, приложение использует устойчивый отпечаток координат, адреса и названия. - Яндекс Карты — проверка карточек всех АЗС со статусом «Вероятно есть»,
для которых T‑Bank передал
yandexOrgId. Яндекс остаётся отдельным источником цен: успешная проверка отображается среди источников АЗС, но не подтверждает фактическое наличие топлива. УстановитеENABLE_YANDEX_PRICES=0, чтобы отключить запросы, либо положительноеYANDEX_PRICE_LIMIT, чтобы ограничить число проверок за поиск. - OpenStreetMap Nominatim — определяет координаты, bbox и административный GeoJSON-контур введённого города или региона. Итоговая сводка отсекает точки за пределами контура; если контур не предоставлен, безопасно используется bbox. Запросы кэшируются на 24 часа и ограничены одним в секунду.
- Sber AZS — API станций проверен через настоящий браузерный сеанс:
/api/stations?bbox=minLon,minLat,maxLon,maxLatвозвращает JSON после выполнения JavaScript-проверки. Приложение запускает один скрытый Chromium-worker в headless-режиме без окна GUI, хранит cookies только во временном профиле и обновляет активные поисковые области каждые 60 секунд. - ГдеБЕНЗ — активный краудсорсинговый источник наличия, очередей и
лимитов:
GET https://gdebenz.ru/api/nearby?lat=...&lon=...&radius_km=.... Запрашивается только при пользовательском поиске и кэшируется на 60 секунд; фонового массового обхода регионов нет. Из радиусного ответа сохраняются только точки внутри найденной территории. Статусы остаются пользовательскими мнениями и показываются со свежестью и подтверждениями.
$env:BENZUP_API_TOKEN = "<полученный Bearer token>"
$env:DEEPSEEK_API_KEY = "<серверный API-ключ>" # только для функций, которые явно используют providers/deepseek.js
$env:ENABLE_ALFA_AZS = "0" # необязательно: полностью отключает Alfa AZS
$env:ENABLE_YANDEX_PRICES = "0" # необязательно: отключает Яндекс Карты
$env:YANDEX_PRICE_LIMIT = "30" # необязательно: ограничивает число проверок
$env:SUMMARY_PROVIDER_TIMEOUT_MS = "8000" # бюджет ожидания каждого основного источника
$env:SBER_SUMMARY_TIMEOUT_MS = "2000" # отдельный бюджет медленного браузерного источника
$env:YANDEX_SUMMARY_TIMEOUT_MS = "4000" # общий бюджет ценового обогащения сводки
$env:CHROME_PATH = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
npm startПодготовленный адаптер DeepSeek использует DEEPSEEK_BASE_URL (по умолчанию
https://api.deepseek.com), DEEPSEEK_MODEL (по умолчанию deepseek-chat) и
DEEPSEEK_TIMEOUT_MS (по умолчанию 60000). Геокодер сначала проверяет исходный
запрос и обращается к нормализатору только при неоднозначном результате; этот
вызов ограничен LOCATION_NORMALIZER_TIMEOUT_MS (по умолчанию 2000). Адаптер приводит
свободный запрос к однозначному названию населённого пункта или административной
территории; городской запрос вида «Воронеж Советский» преобразуется в название
района с контекстом города и региона. Лимит ответа задаёт
DEEPSEEK_NORMALIZER_MAX_TOKENS (по умолчанию 2000). Результат модели
обязательно подтверждается Nominatim, а при недоступности DeepSeek используется
исходная формулировка. Ключ остаётся
только на сервере и не должен попадать в браузер, исходный код или логи.
BENZUP_API_URL позволяет заменить базовый endpoint, если BenzUp выдаст другой
адрес в договоре. Токены не должны попадать в исходный код, браузер или логи.
CHROME_PATH необязателен на Windows, если Chrome или Edge установлен в
стандартную папку. Для browser-worker требуется Node.js 22 или новее. Worker
создаётся по первому поиску, обслуживает до 10 активных областей, удаляет
неактивные через 15 минут и очищает временный профиль при штатной остановке.
CHROME_NO_SANDBOX=1 является ослаблением защиты и не используется при обычном
локальном запуске. Штатный Compose включает его явно, потому что Chromium уже
работает непривилегированно в read-only контейнере с no-new-privileges, которое
несовместимо с setuid sandbox. Публичные HTTP-запросы ограничиваются параметрами REQUEST_RATE_*;
для /api/cache/refresh действует отдельный более строгий лимит. Очередь Nominatim
дополнительно ограничена GEOCODER_QUEUE_MAX, чтобы входящие запросы не могли
создать неограниченный backlog при обязательной паузе между геокодированиями.
Для публичного размещения задайте идентифицирующий User-Agent с контактами:
$env:GEOCODER_USER_AGENT = "BenzAI/1.0 (contact: admin@example.ru)"GET /api/location?q=Воронеж%20Советский— быстро определяет территорию и её bbox без ожидания источников АЗС. Интерфейс сразу фокусирует карту и запускает потоковую загрузку видимой области параллельно с полной сводкой.GET /api/summary?q=Воронежская%20область— территория, полная сводка и АЗС.GET /api/stations?minLat=...&maxLat=...&minLon=...&maxLon=...— нормализованные АЗС и предупреждения по источникам для текущей области карты. Для карты добавляетсяmode=viewport; одна сторона bbox не должна превышать 12 градусов.GET /api/stations/stream?...&mode=viewport— NDJSON-поток кумулятивных снимков АЗС; клиент карты обновляет маркеры после каждой строки, не ожидая завершения всех источников.
При текстовом поиске /api/location и /api/summary запускаются параллельно. После ответа геокодера карта начинает получать /api/stations/stream, поэтому первые маркеры появляются до готовности полной сводки. Финальная сводка объединяется с уже показанными станциями без очистки карты.
Ответ содержит sources с состоянием каждого провайдера. У станции поля
fuelStatus и overallStatus относятся к вероятностному наличию, а prices и
priceUpdatedAt — к цене. Эти сигналы намеренно не подменяют друг друга.
GET /api/health показывает состояние сервера и Chromium-worker Sber.
POST /api/cache/refresh?q=Воронеж очищает общую сводку, геокодирование,
Multigo, ГдеБЕНЗ, Sber и цены Яндекса, затем возвращает полностью пересобранный
ответ с метаданными cacheRefresh.
Если ответ T-Bank достигает предполагаемого лимита в 300 записей, сервер автоматически делит территорию на меньшие части и удаляет дубликаты. Поэтому поиск подходит не только для городов, но и для областей.
Ответы T-Bank не являются официально документированным контрактом. Интерфейс поэтому показывает вероятностные формулировки статусов, а не гарантию наличия топлива.