Интерактивная карта точек, розеток и маршрутов для райдеров на моноколёсах в Алматы. Помогает планировать поездки, находить места встреч и розетки для подзарядки, а также видеть геопозиции участников Telegram-чатов в реальном времени и сеть городских велодорожек.
Ссылка: https://map.euc.kz
Статус: Production (React 19 + Mapbox GL + Supabase + PWA)
Архитектура: Подробно описана в docs/ — для новых инженеров и архитектурных обзоров. Правила разработки — в AGENTS.md.
Одностраничный PWA на базе Mapbox GL JS с несколькими тематическими слоями. Статические данные (точки, маршруты) кэшируются агрессивно; только Telegram-локации обновляются в реальном времени через Supabase Realtime (обновление <500ms).
- Точки — места встреч, парковки, точки интереса (модерируются администратором).
- Розетки — публичные точки подзарядки моноколёс.
- Маршруты — заранее проложенные треки для покатушек (с опциональными высотами).
- Велодорожки — сеть велоинфраструктуры Алматы (статический датасет Velojol).
- Геопозиции Telegram — живые геопозиции райдеров из подключённых чатов + недавние треки (TTL-фильтр, точность фильтр).
- Переключение подложки: Карта (Mapbox Streets) ↔ Спутник (сохраняется в localStorage).
- Детали фичи: Сайдбар с информацией, фотогалерея, координаты, поделиться ссылкой.
- Deep links:
/m/point/11,/m/route/5— прямая ссылка на фичу с автозумом (легаси#point=11редиректится); события —/events/:id. - Offline-first: Service Worker кэширует app shell, статические ассеты, Mapbox-тайлы. Карта работает офлайн.
- PWA: Установима на iOS/Android (splash-screens для всех современных устройств).
- Интерактивность: Hover/click-эффекты через Mapbox
feature-state(без React re-renders!). - Видимость слоёв: Запоминается в localStorage; быстрая кнопка сброса кеша при ошибке.
- Safe Area: Корректные отступы для iOS notch и home indicator.
Любой посетитель может предложить новую точку или розетку (вкладка «Добавить точку»):
- Клик на карте → выбор координат.
- Заполнение формы (тип, название, описание, флаги).
- Заявка отправляется в таблицу
map_points_submissions(статус: pending). - Администратор проверяет в
/admin/submissions, одобряет или отклоняет. - Одобренная точка появляется на карте всем пользователям.
Edge Function telegram-location-bot — webhook для Telegram-бота:
- Сбор локаций: Бот, добавленный в чат, получает обновления локаций.
- Сохранение: INSERT в
telegram_locationsс координатами, user ID, chat ID. - Профиль пользователя: Автоматически подтягивает/кэширует аватар в
telegram_profiles. - Realtime broadcast: Supabase отправляет изменение на фронтенд.
- Отображение: На карте видны только свежие точки в пределах TTL и погрешности.
Фильтры (переменные окружения):
VITE_TELEGRAM_GEO_TTL_MINUTES(default: 60) — сколько минут показывать локацию.VITE_TELEGRAM_MAX_ACCURACY_METERS(default: 100) — максимальная погрешность GPS.VITE_TELEGRAM_TRACK_TAIL_MINUTES(default: 30) — длина недавнего трека.
- Frontend: React 19, TypeScript, Vite 8, TailwindCSS 4.
- Карта: Mapbox GL JS 3 с пользовательским стилем.
- Backend / БД: Supabase (PostgreSQL + Row Level Security + Storage + Edge Functions на Deno).
- Аналитика: Яндекс.Метрика (опционально).
- Качество кода: ESLint, Prettier, Vitest для unit-тестов.
- Деплой: GitHub Pages с кастомным доменом
map.euc.kz(см.CNAME).
src/
├── components/ # Основные: EucMap (оркестратор), LayerControls, FeatureSidebar, AddPointPanel
├── hooks/ # Ключевые: useMapData (fetch), useMapbox (instance), useLayers, useTelegramRealtime
├── lib/
│ ├── supabase.ts # Все API-вызовы, timeout+retry, нормализация
│ └── mapLayers.ts # Определения слоёв, paint expressions, feature-state
├── utils/ # Геометрия, hash-навигация, типовые гварды, GeoJSON нормализация
├── constants/ # LAYER_IDS, SOURCE_IDS, COLORS
├── types/ # GeoJSON Features, Supabase rows, Velojol
├── data/ # almaty.json (велодорожки)
└── main.tsx, App.tsx
supabase/
├── migrations/ # DDL: таблицы, RLS, Storage, индексы
├── functions/
│ └── telegram-location-bot/index.ts # Deno: webhook, avatar fetch, INSERT
└── schema.sql # Full export
public/
├── sw.js # Service Worker: cache strategy (STATIC, RUNTIME, TILES)
├── manifest.webmanifest
├── favicon.svg, icons/, splash screens
Для понимания архитектуры: см. docs/ — диаграммы, data flow, схема БД, бот, тесты и деплой.
Это read-heavy, realtime-optional система:
- Статические данные (точки, маршруты) → Supabase → Frontend кэширует агрессивно
- Telegram-локации → Webhook → Edge Function → Supabase Realtime → Frontend обновляется <500ms
- Service Worker → App shell работает офлайн, Mapbox-тайлы кэшируются
- Mapbox feature-state → Hover/select эффекты без React re-renders
Пользователь открывает карту
→ EucMap монтируется
→ useMapData параллельно фетчит из Supabase (points, routes, telegram, velojol)
→ useLayers добавляет GeoJSON sources + layers в Mapbox
→ useTelegramRealtime подписывается на изменения
→ Карта готова к взаимодействию
Пользователь кликает на точку
→ Mapbox click event → useMapClick слушатель
→ feature-state selected=true → подсветка в Mapbox
→ URL hash обновляется (#point-123)
→ FeatureSidebar рендерится (React)
Telegram-райдер отправляет локацию в чат
→ Telegram API → Edge Function webhook
→ Скачивается аватар, удаляется bot token, загружается в Storage
→ INSERT telegram_locations
→ Supabase Realtime broadcast
→ Frontend refreshTelegramUsers() → GeoJSON обновляется
→ Mapbox source обновляется → карта перерисовывается
Требуется Node.js 20+ и npm.
# Установка зависимостей
npm install
# Конфигурация окружения
cp .env.example .env.local
# и заполните VITE_MAPBOX_TOKEN, VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY
# Запуск dev-сервера
npm run dev
# Сборка production-бандла
npm run build
# Локальный preview собранной версии
npm run preview
# Тесты и линт
npm test
npm run lint| Переменная | Назначение |
|---|---|
VITE_MAPBOX_TOKEN |
Публичный токен Mapbox для рендеринга карты. |
VITE_SUPABASE_URL |
URL проекта Supabase. |
VITE_SUPABASE_PUBLISHABLE_KEY |
Anon-ключ Supabase для чтения публичных данных. |
VITE_YANDEX_METRIKA_ID |
ID счётчика Яндекс.Метрики (необязательно). |
VITE_TELEGRAM_GEO_TTL_MINUTES |
Сколько минут показывать геопозиции Telegram-пользователей. |
VITE_TELEGRAM_TRACK_TAIL_MINUTES |
Длина «хвоста» трека по времени. |
VITE_TELEGRAM_MAX_ACCURACY_METERS |
Максимально допустимая погрешность координат, м. |
Для разработки и правки логики бота, миграций и RLS работайте с локальным стеком — это быстро, бесплатно и оффлайн, без облачных preview-веток (они требуют платного плана). Локальный стек поднимает полный Supabase в Docker: Postgres, Auth, Storage, Realtime, Studio и Edge Runtime.
supabase start # поднять весь стек (Docker)
supabase status # URL'ы и ключи (anon/service_role) для .env.local
supabase stop # остановитьПорты заданы в supabase/config.toml: API 54321, DB 54322, Studio 54323.
Миграции и RLS — правьте файлы в supabase/migrations/, затем пересоздайте локальную БД (применит миграции + seed.sql):
supabase db resetНа удалённый проект миграции применяются через
supabase db pushили CI — не через MCPapply_migration, иначе история миграций разойдётся.
Edge Function (Telegram-бот) — запуск локально с hot-reload:
supabase functions serve telegram-location-bot
# доступна по http://127.0.0.1:54321/functions/v1/telegram-location-botФункция задекларирована в supabase/config.toml секцией [functions.telegram-location-bot] с verify_jwt = false — вебхук Telegram приходит без Supabase JWT, аутентификация идёт по secret-токену внутри функции. Эта же декларация нужна, чтобы функция автоматически деплоилась в облачные branch-окружения.
Telegram-вебхуку нужен публичный HTTPS-URL, поэтому для теста реального вебхука пробросьте локальную функцию наружу туннелем (ngrok / cloudflared) либо используйте облачную preview-ветку (Pro-план).
Маршрут /admin (на проде: https://map.euc.kz/admin): модерация заявок, CRUD точек и маршрутов, фото, просмотр live Telegram-трекинга.
- Тип: Supabase Auth (email/пароль)
- Авторизация: Проверка
map_admin_usersтаблицы через RLS - Безопасность: Service role ключ не попадает в браузер (Supabase блокирует). Только anon ключ.
- Сессия: Хранится в localStorage; refresh token управляется SDK
-
В Supabase включите провайдер Email (Authentication → Providers → Email).
-
Создайте пользователя (Authentication → Users → Add user) или зарегистрируйтесь через включённый signup — как удобнее для вашего проекта.
-
Узнайте
uuidпользователя в списке Users. -
Примените миграции (
supabase db pushили через Dashboard → SQL), затем выдайте права администратора:INSERT INTO public.map_admin_users (user_id) VALUES ('<uuid-пользователя-из-auth-users>') ON CONFLICT (user_id) DO NOTHING;
-
Откройте
/admin, войдите email/паролём.
Загрузка фото в Storage разрешена только администраторам (RLS на storage.objects и таблицы данных). Публичная анонимная загрузка в бакет точек отключена миграцией.
SPA на GitHub Pages: в dist/ появляется 404.html (копия index.html), чтобы прямые переходы по путям отдавали приложение и React Router обрабатывал маршруты.
Реализован через Edge Function telegram-location-bot. Он принимает update от Telegram и сохраняет сообщения с location в таблицу telegram_locations. Аватары пользователей сохраняются в Storage-бакет telegram-avatars, а в telegram_profiles.avatar_url хранится безопасный public URL без bot token.
Развёртывание:
- Задайте секреты для функции:
Опционально:
supabase secrets set TELEGRAM_BOT_TOKEN=<bot_token> \ TELEGRAM_WEBHOOK_SECRET=<random_secret> \ TELEGRAM_BACKFILL_SECRET=<другой_секрет_только_для_backfill>
TELEGRAM_BACKFILL_MAX_PROFILES— лимит профилей за один вызов backfill (по умолчанию 500). Альтернатива: заполнить эти переменные в.env.localи выполнитьnpm run secrets:sync— скрипт зальёт все заполненные секреты edge-функций разом. - Задеплойте функцию (на проде при каждом push в
mainэто делает GitHub Actions; вручную — например для первого запуска или отладки):supabase functions deploy telegram-location-bot --no-verify-jwt --use-api
- Подключите webhook у бота (без передачи bot token в URL):
curl -X POST "https://api.telegram.org/bot<bot_token>/setWebhook" \ -H "Content-Type: application/json" \ -d '{"url":"https://<project-ref>.supabase.co/functions/v1/telegram-location-bot","secret_token":"<random_secret>"}'
После этого любые сообщения с геопозицией в чате, где есть бот, будут попадать в telegram_locations и автоматически отображаться на карте.
Если нужно переобновить аватары для уже заполненных telegram_profiles (например, после security-фикса), запустите backfill:
curl -X POST "https://<project-ref>.supabase.co/functions/v1/telegram-location-bot/backfill" \
-H "x-telegram-backfill-secret: <TELEGRAM_BACKFILL_SECRET>"Если ответ содержит capped_at_max_profiles: true, повторите запрос с параметром ?from=<next_from> из JSON (продолжение по порядку строк).
Функция пройдёт по telegram_profiles и обновит только небезопасные/пустые avatar_url.
Та же Edge Function telegram-location-bot обслуживает рассылку анонсов дат событий и обработку кнопки «Участвую»:
- Сабрут
/announce(POST, авторизация по JWT администратора): отправляет анонс конкретной даты события в выбранные чаты с инлайн-кнопкой «Участвую». Вызывается из админки черезsupabase.functions.invoke('telegram-location-bot/announce', …). - Сабрут
/announce-cancel(POST, JWT администратора): при отмене даты редактирует все её сообщения в «❌ ОТМЕНЕНО» и убирает кнопку. callback_query: нажатие «Участвую» в любом чате — toggle участия поtelegram_user_id(повторный тап убирает запись), счётчик в подписи кнопки обновляется. Профиль участника при необходимости создаётся вtelegram_profiles(без аватара — его добьёт backfill).
Список чатов для рассылки хранится в таблице telegram_chats (управляется в админке /admin/telegram-chats, не через env). Участники — в map_event_participants, отправленные сообщения — в telegram_outbound_messages (общая таблица для анонсов событий и новостей проекта). Новых секретов не требуется: используются существующие TELEGRAM_BOT_TOKEN, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY. Подробно: docs/telegram-bot.md.
Edge Function ai-assist улучшает название/описание точек и маршрутов через OpenAI (Responses API + web-поиск) — кнопка «Улучшить с ИИ» на страницах редактирования в админке. Авторизация — JWT администратора + map_admin_users. Подробно: docs/admin.md.
Настройка:
- Впишите
OPENAI_API_KEY(и опциональноOPENAI_MODEL, по умолчаниюgpt-5-mini) в.env.local. - Залейте секреты в Supabase (скрипт заливает все заполненные секреты edge-функций из
.env.local, включаяTELEGRAM_*):npm run secrets:sync
- Деплой функции — автоматически при push в
main; вручную:supabase functions deploy ai-assist --no-verify-jwt --use-api
# Проблема: изменения не видны после деплоя
# Решение: очистить все кэши
- DevTools → Application → Service Workers → Unregister
- Clear All (Storage)
- Hard refresh (Ctrl+Shift+R или Cmd+Shift+R)
# Или: используй кнопку "Сброс кеша" в приложении// DevTools console:
map.getStyle() // Проверить стиль
map.getSources() // Список источников
map.getLayers() // Список слоёв
map.querySourceFeatures(sourceId) // Данные в источнике- Проверить
.env.local:VITE_SUPABASE_URL,VITE_SUPABASE_PUBLISHABLE_KEY - Протестировать в Supabase Dashboard → SQL Editor (SELECT 1 как anon)
- Supabase Dashboard → Edge Functions →
telegram-location-bot→ Logs - Проверить
TELEGRAM_WEBHOOK_SECRETсовпадает с Telegram API - Ручной тест webhook (см. docs/telegram-bot.md)
| Таблица | Цель |
|---|---|
map_points |
Точки и розетки (type: point | socket), флаги: meeting, socket, disabled |
map_routes |
Маршруты (LineString с опциональными высотами, via_coordinates) |
map_points_submissions |
Очередь модерации (status: pending | approved | rejected) |
map_point_photos |
Фото (FK → points), Storage bucket references |
map_admin_users |
Администраторы (FK → auth.users), заполняется вручную |
telegram_locations |
Живые локации (с фильтром TTL в коде) |
telegram_profiles |
Кэш аватаров, имён, ников Telegram-пользователей |
map_events |
События (покатушки/мероприятия/обучение) + фото, старт/финиш |
map_event_dates |
Даты проведения событий (с отменой) |
map_event_participants |
RSVP «Участвую» из Telegram (toggle) |
map_news |
Новости проекта (только рассылка, мягкое удаление) |
telegram_chats |
Чаты/темы для рассылки анонсов |
telegram_outbound_messages |
Исходящие сообщения бота (анонсы событий ЛИБО новости) |
RLS: Публичные таблицы: чтение анонимно (где flag_disabled = false), запись только администраторам. Полная матрица — в docs/database.md.
- Прочитай этот README + docs/architecture.md
npm install && npm run dev- Открой
http://localhost:5173в браузере - Кликни на точку → смотри как это работает в DevTools
- Откройте DevTools → Console, Application, Network
- Кликните на точку на карте → watch
useMapClicktrigger →feature-stateupdated → Mapbox перерисует - Переключите видимость слоя → watch localStorage update
- Посмотрите код в
src/components/EucMap.tsx(оркестратор),src/hooks/useMapData.ts(fetch),src/lib/mapLayers.ts(слои)
Низкий риск: Добавь новый toggle для слоя (копируй существующий паттерн)
Средний риск: Измени TTL или max accuracy для Telegram
Высокий риск: Трогай RLS policies, paint expressions, Mapbox feature-state logic
Не модифицируй впервые без очень хорошей причины:
src/lib/mapLayers.ts— paint expression ошибки → невидимые слоиsupabase/migrations/— RLS ошибки → утечка данныхsrc/hooks/useMapData.ts— race condition → stale datasupabase/functions/telegram-location-bot/index.ts— утечка bot token
Иконки и splash-screens генерируются из public/favicon.svg:
npm run generate:pwa-icons
npm run generate:pwa-startupЧто: Mapbox feature-state с paint expressions для hover/select вместо DOM обновления.
// No React re-render! Pure Mapbox update:
map.setFeatureState({ source, id }, { selected: true })
// Paint expr: ["case", ["feature-state", "selected"], selectedColor, defaultColor]Зачем: Ultra-smooth interactions без JavaScript bottleneck. Hover на 10K точек → zero lag.
Что: Если одна из 4 фетчей упадёт, карта всё равно грузится с остальными данными.
const [pointsResult, routesResult, telegramResult, bikeLanesResult]
= await Promise.allSettled([...])
// Даже если одна rejected, остальные fulfilled работаютЗачем: Degrade gracefully. Telegram down? Остальные слои видны.
Что: Каждый API-вызов обёрнут в withTimeoutAndRetry (10s timeout, 2 retries, экспоненциальная задержка).
Зачем: Надёжность на плохом интернете. На мобильных сетях часто бывают висящие запросы.
Что: Три кэша: STATIC (app shell), RUNTIME (pages), TILES (Mapbox tiles).
Зачем: Приложение работает офлайн. Тайлы кэшируются forever (immutable by URL).
Что: Только telegram_locations и telegram_profiles на живой подписке. Остальное batch-fetch.
Зачем: Simpler. Eventual consistency OK для points/routes (не меняются часто).
Сборка для GitHub Pages:
GITHUB_PAGES=true npm run buildПри сборке в режиме GitHub Pages vite.config.ts подменяет base на /map.euc/, а плагин baseUrlMetaPlugin подставляет корректные абсолютные URL в OG-метатеги index.html. Кастомный домен закреплён файлом CNAME.
Workflow .github/workflows/deploy.yml перед сборкой фронтенда:
- Выполняет
supabase db pushк проекту (версия CLI берётся изpackage-lock.json, см. зависимостьsupabase). - Деплоит Edge Function
telegram-location-botкомандойsupabase functions deploy telegram-location-bot --no-verify-jwt --use-api(--no-verify-jwtнужен для webhook Telegram без JWT;--use-api— сборка функции на стороне Supabase без Docker на раннере).
Секреты самой функции (TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, …) в CI не задаются: их один раз прописывают в проекте через supabase secrets set или Dashboard (см. раздел «Telegram-бот» выше). Деплой только обновляет код функции.
В настройках репозитория GitHub нужны:
| GitHub Variables | Значение |
|---|---|
SUPABASE_PROJECT_REF |
Идентификатор проекта из URL дашборда: https://app.supabase.com/project/<SUPABASE_PROJECT_REF> (совпадает с поддоменом https://<SUPABASE_PROJECT_REF>.supabase.co). |
VITE_MAPBOX_TOKEN |
Публичный токен Mapbox (см. раздел «Переменные окружения»). |
VITE_SUPABASE_URL |
URL проекта Supabase. |
VITE_SUPABASE_PUBLISHABLE_KEY |
Anon / publishable-ключ. |
VITE_YANDEX_METRIKA_ID |
ID Яндекс.Метрики; допускается пустая строка. |
VITE_TELEGRAM_GEO_TTL_MINUTES |
TTL геометок Telegram на карте. |
VITE_TELEGRAM_TRACK_TAIL_MINUTES |
Длина «хвоста» трека. |
VITE_TELEGRAM_MAX_ACCURACY_METERS |
Макс. погрешность координат, м. |
| GitHub Secrets | Значение |
|---|---|
SUPABASE_ACCESS_TOKEN |
Personal access token Supabase. |
SUPABASE_DB_PASSWORD |
Пароль базы данных проекта (Settings → Database). |
TELEGRAM_BOT_TOKEN |
Токен бота для уведомлений о результате деплоя в Actions (не путать с секретом Edge Function — там свой экземпляр в Supabase). |
TELEGRAM_CHAT_ID |
Чат или канал для этих уведомлений. |
Локальные шаблоны со списком имён (в .gitignore, не коммитятся): .env.github_vars, .env.github_secrets.
Синхронизация переменных и секретов через gh
Из корня репозитория, после заполнения файлов:
Variables (репозиторий по умолчанию; для другого репозитория добавьте -R owner/repo):
set -a
source .env.github_vars
set +a
gh variable set SUPABASE_PROJECT_REF --body "$SUPABASE_PROJECT_REF"
gh variable set VITE_MAPBOX_TOKEN --body "$VITE_MAPBOX_TOKEN"
gh variable set VITE_SUPABASE_URL --body "$VITE_SUPABASE_URL"
gh variable set VITE_SUPABASE_PUBLISHABLE_KEY --body "$VITE_SUPABASE_PUBLISHABLE_KEY"
gh variable set VITE_YANDEX_METRIKA_ID --body "$VITE_YANDEX_METRIKA_ID"
gh variable set VITE_TELEGRAM_GEO_TTL_MINUTES --body "$VITE_TELEGRAM_GEO_TTL_MINUTES"
gh variable set VITE_TELEGRAM_TRACK_TAIL_MINUTES --body "$VITE_TELEGRAM_TRACK_TAIL_MINUTES"
gh variable set VITE_TELEGRAM_MAX_ACCURACY_METERS --body "$VITE_TELEGRAM_MAX_ACCURACY_METERS"Secrets:
set -a
source .env.github_secrets
set +a
gh secret set SUPABASE_ACCESS_TOKEN --body "$SUPABASE_ACCESS_TOKEN"
gh secret set SUPABASE_DB_PASSWORD --body "$SUPABASE_DB_PASSWORD"
gh secret set TELEGRAM_BOT_TOKEN --body "$TELEGRAM_BOT_TOKEN"
gh secret set TELEGRAM_CHAT_ID --body "$TELEGRAM_CHAT_ID"