История о том, как боль от «телепортов» в GPS‑треках, платных зон и неудобного создания файлов для Garmin привела к рождению pet‑проект а, который немного вышел из-под контроля. Разбор стека, подводных камней и немного партизанского кодинга.

Приветственная страница

Всем привет! Меня зовут Александр, я разработчик и, как многие в IT-сообществе, увлеченный велосипедист. Strava для меня — основной инструмент для отслеживания прогресса, но с годами я стал все чаще натыкаться на мелкие «но», которые мешали полноценному анализу.

Знакомы эти боли?

  • «Телепорты» в GPX‑треках. Проехал туннель или попал в «мертвую» зону GPS, и вот на треке уже красуется прямая линия на пару километров, которая убивает всю статистику.

  • Анализ по зонам. Strava предоставляет его только по платной подписке. А если у тебя свои, кастомные зоны, рассчитанные тренером? Увы.

  • Live Segments на Garmin. Чтобы гоняться с «призраком» (своим PR или KOM'ом) на сегменте, нужно вручную скачивать его с сайта, конвертировать и загружать на устройство. Утомительно.

В какой‑то момент количество этих «но» превысило критическую массу, и во мне проснулся разработчик. Я решил: «А что, если сделать инструмент, который решит все эти проблемы для меня?». Так и родился Peakline.

Сегодня я хочу рассказать, как из этой идеи вырос полноценный веб‑сервис, с какими техническими вызовами я столкнулся и что получилось в итоге.

Панель с сводкой данных
Панель с сводкой данных

Технологический стек: просто, быстро, асинхронно

Для такого проекта не хотелось тащить тяжелые фреймворки. Выбор пал на связку, которая позволяет быстро разрабатывать и легко поддерживать сервис в одиночку:

  • Бэкенд: FastAPI на Python. Асинхронность «из коробки» идеальна для работы с внешними API, а Pydantic — для валидации данных.

  • Фронтенд: Vanilla JS. Никаких React или Vue. Для такого интерфейса достаточно «чистого» JS, Chart.js для графиков и Leaflet для карт. Шаблоны рендерятся на сервере с помощью Jinja2.

  • База данных: SQLite. Да‑да, для pet‑проекта, который может и не взлететь, это идеальный выбор. Никаких забот с настройкой и администрированием.

  • Работа с API: AIOHTTP для асинхронных запросов к Strava и requests для редких синхронных задач.

Киллер-фичи и как они устроены "под капотом"

1. FIT-генератор Pro-уровня: Гонка с целью

Это была одна из главных задач. Я хотел нажать одну кнопку и получить готовый.fit файл для Garmin, где в качестве соперника будет не просто абстрактный лидер, а конкретная цель.

Модальное окно с выбором цели в .fit файл
Модальное окно с выбором цели в.fit файл

Проблема: Формат FIT — это бинарный протокол. Просто так его не сгенерируешь.
Решение: Я нашел отличную, хоть и не очень известную, библиотеку fit‑segment‑encoder. Она берет на себя всю грязную работу по упаковке данных в бинарный формат. Моя задача свелась к тому, чтобы правильно подготовить данные.

Вот как это работает:

  1. Пользователь нажимает «Скачать FIT» и в модальном окне выбирает цель: KOM/QOM, свой PR или вводит время вручную (например, 5m30s).

  2. Бэкенд получает детали сегмента через Strava API, включая трек (потоки latlng, distance, altitude) и время KOM/PR.

  3. Затем для каждой точки трека рассчитывается «идеальное» время прохождения, исходя из выбранной цели и общей дистанции (пока что линейно).

  4. Все эти данные (координаты, высота, дистанция, время лидера) скармливаются FitSegmentEncoder.

  5. Библиотека генерирует байты, которые я отдаю пользователю с правильным MIME‑типом application/vnd.ant.fit.

Модальное окно с выбором цели в.fit файл
Скриншот отрывка fit_service.py

2. Визуальный GPX Fixer

«Телепорты» в треках — моя личная боль. Я решил, что просто чинить их мало, нужно показывать, что именно было исправлено.

Проблема: Как найти «прыжок» в треке и как наглядно это показать?
Решение:

  1. Бэкенд: Использую библиотеку gpxpy для парсинга загруженного GPX. Прохожу по всем точкам трека и вычисляю расстояние между соседними. Если point.distance_2d(last_point) > threshold_meters, считаю это «прыжком» и начинаю новый сегмент в GPX‑файле.

  2. Фронтенд: После обработки бэкенд возвращает не только исправленный файл, но и два полилайна (закодированных в polyline): оригинальный и исправленный. На фронте я просто рисую на карте Leaflet два слоя: красный (сломанный трек) и зеленый (исправленный). Сразу видно, какие «петли» и «прыжки» были удалены.

Фиксер и два слоя: красный (сломанный трек) и зеленый (исправленный)
Фиксер и два слоя: красный (сломанный трек) и зеленый (исправленный)

3. Интерактивная связка "Карта-График"

На странице анализа тренировки хотелось не просто смотреть на графики мощности или пульса, но и понимать, где именно на маршруте был тот или иной пик.

Проблема: Как синхронизировать положение курсора на графике с маркером на карте?
Решение: Это магия JavaScript.

  1. Chart.js предоставляет колбэк onHover. Он срабатывает при наведении мыши на график и передает информацию об активных элементах.

  2. Из этого события я получаю dataIndex — индекс точки данных, на которую наведен курсор.

  3. У меня уже есть массив с GPS-координатами (latlng stream), полученный от Strava API. Я просто беру из этого массива координаты с тем же индексом: const coords = latlngStream[dataIndex].

  4. Остается только обновить позицию заранее созданного маркера на карте Leaflet (trackMarker.setLatLng(coords)) и добавить его на карту, если он еще не там. Когда курсор уходит с графика, я просто убираю маркер с карты.

Эффект получился лучше чем я полагал.

4. Детальный анализ активностей и кастомные зоны

Strava предоставляет отличный базовый анализ, но если вы хотите копать глубже, начинаются нюансы. Например, зоны мощности и пульса рассчитываются по стандартным формулам. А если у вас есть данные с лабораторного теста или вы используете методику, отличную от стандартной?

Решение:

  1. Кастомные зоны: Я добавил в профиле пользователя страницу настроек, где можно вручную задать границы для каждой из 5 зон пульса и 7 зон мощности. Эти данные сохраняются в нашей базе SQLite.

  2. Умный расчет: Когда вы открываете страницу анализа тренировки, бэкенд делает следующее:

    • Запрашивает у Strava все доступные потоки данных (streams) для активности: пульс, мощность, скорость, каденс и т.д.

    • Проверяет, задал ли пользователь свои кастомные зоны.

    • Если да — время в зонах рассчитывается на сервере с помощью Python (numpy отлично подходит для быстрых вычислений по массивам данных).

    • Если нет — мы используем данные, которые отдает Strava API (если у пользователя есть подписка) или считаем по стандартным процентам от FTP/Max HR.

Это дает пользователю полный контроль над анализом, а мне — интересный вызов по работе с "сырыми" данными.

Фрагмент логики из activity_analyzer.py
Фрагмент логики из activity_analyzer.py

5. Segment Hunter: Поиск новых вызовов

Иногда хочется найти новые интересные сегменты для тренировок рядом с домом, но интерфейс Strava для этого не всегда удобен.

Решение: Я сделал интерактивную карту "Охотника за сегментами".

  • Пользователь перемещает карту в нужную область.

  • При нажатии на кнопку «Найти» фронтенд отправляет на бэкенд координаты видимой области карты (bounds).

  • Бэкенд использует эндпоинт Strava API /segments/explore, который возвращает сегменты в заданном прямоугольнике.

  • Найденные сегменты отображаются на карте иконками. При наведении на иконку подсвечивается трек сегмента, а при клике — открывается попап с его статистикой и кнопками для скачивания FIT‑файла или перехода на Strava.

Просто, но очень эффективно для планирования новых маршрутов.

Интерактивная карта поиска сегментов
Интерактивная карта поиска сегментов

6. Экспериментальные функции: ищем создателя сегмента

А теперь немного о функциях «для своих». В Strava нет официального способа узнать, кто создал тот или иной сегмент. Иногда это полезно, чтобы понять логику создателя или просто из любопытства. Я решил, что эту информацию можно достать, но доступ к ней будет не для всех.

Фрагмент кода из segment_helpers.py
Фрагмент кода из segment_helpers.py

Проблема: Официальное API V3 не отдает ID создателя сегмента.
Решение: «Партизанский» подход. Я обнаружил, что старые, недокументированные эндпоинты, которые используются внутренним фронтендом Strava, все еще работают, если отправить запрос с правильными Cookie. Достать их можно из своего браузера.

Модальное окно с владельцем сегмента
Модальное окно с владельцем сегмента

Как это работает в Peakline:

  1. «Секретный клуб»: Чтобы получить доступ к этой и другим экспериментальным фичам, пользователь должен привязать свой Telegram‑аккаунт к профилю Peakline. Это мой способ ограничить доступ к «серым» функциям и дать их только самым заинтересованным пользователям.

  2. Пул аккаунтов: На сервере у меня есть пул из нескольких «технических» аккаунтов Strava, для каждого из которых сохранен Cookie заголовок.

  3. Запрос: Когда пользователь из «клуба» запрашивает создателя сегмента, бэкенд берет куки из пула и делает синхронный requests‑запрос на старый эндпоинт (/athlete/segments/{id}/history).

  4. Отказоустойчивость: Если один аккаунт из пула забанят или его сессия истечет, система автоматически переключится на следующий. Это обеспечивает относительную стабильность функции.

Что в итоге?

В результате этих изысканий родился проект Peakline (ссылка активная, можно заходить и пробовать).

Что уже работает:

  • Личный кабинет с полной статистикой и последними активностями.

  • Анализ сегментов с интерактивной картой и профилем высот.

  • Скачивание FIT и GPX файлов для Garmin с выбором цели.

  • GPX Fixer для исправления «телепортов».

  • Segment Hunter для поиска новых вызовов на карте.

  • Ручная настройка зон мощности и пульса для точного анализа.

Планы на будущее и обратная связь

Проект только в начале пути. В планах добавить более глубокий AI‑анализ и много других мелочей. Этим проектом я занимаюсь в свободное время, но он вырастет благодаря отзывам сообщества.

Буду рад, если вы попробуете Peakline и поделитесь своими впечатлениями, идеями или сообщениями об ошибках прямо в комментариях к этой статье. Спасибо за внимание, Хабр!

Комментарии (10)


  1. GAG
    20.06.2025 17:24

    Вижу, что пока ещё не всё переведено на русский, но уже радует, что нет искусственных достижений по количеству пользователей и обработанной информации.

    Выглядит правдоподобно:

    Our Community in Numbers

    Unique Visitors

    • 246

    Happy Athletes

    • 7

    Activities Analyzed

    • 26

    FIT Files Created

    • 4


    1. cyberscoper Автор
      20.06.2025 17:24

      Да, это верно вот параллельно добавляю перевод.


    1. cyberscoper Автор
      20.06.2025 17:24

      Исправлен недоперевод
      Исправлен недоперевод

      так то лучше выглядит :)


  1. pnmv
    20.06.2025 17:24

    Порадовала подсветка кода, на снимках экрана. Цветовая схема - для настоящих красноглазиков.


  1. megalloid
    20.06.2025 17:24

    Правда качество работы GPS сейчас "малость" расстраивает...


    1. cyberscoper Автор
      20.06.2025 17:24

      В целом? или конкретно нюансы в работе моего "фиксера"


      1. Kwisatz
        20.06.2025 17:24

        во многих городах, например в Казани GPS просто бесполезен во всем центре, например.

        По статье: очень круто, вы кросавчег)


        1. cyberscoper Автор
          20.06.2025 17:24

          Благодарю.
          Я тут и за фронт и за бэк у проекта есть достаточное количество проблем но они решаемы.
          А вот с GPS - мало что поделать это грустно


  1. cmyser
    20.06.2025 17:24

    Тут бы локал first идеально зашел

    Чисто с телефона данные получать о перемещениях

    Знаю одну возможность сделать это в браузере


    1. cyberscoper Автор
      20.06.2025 17:24

      Ну выкладывай :) глянуть полезно