From 6edf97fe79cd53014acaf8bc558228e339ab1de2 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 3 Jun 2026 17:33:09 +0000 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=59 --- docs/work-items/ET-006/01-brd.md | 120 ++-- docs/work-items/ET-006/02-trz.md | 494 ++++++++--------- .../ET-006/03-acceptance-criteria.md | 428 ++++++++------- docs/work-items/ET-006/04-test-plan.yaml | 512 ++++++++++-------- docs/work-items/ET-006/04b-ui-test-cases.md | 211 ++++++++ 5 files changed, 1055 insertions(+), 710 deletions(-) create mode 100644 docs/work-items/ET-006/04b-ui-test-cases.md diff --git a/docs/work-items/ET-006/01-brd.md b/docs/work-items/ET-006/01-brd.md index 41f43c3..8778a3c 100644 --- a/docs/work-items/ET-006/01-brd.md +++ b/docs/work-items/ET-006/01-brd.md @@ -1,84 +1,106 @@ --- type: brd work_item_id: ET-006 -title: "BRD: Загрузка и визуализация GPX-треков" -version: 2 +title: "BRD: Скачивание трека из popup на карте" +version: 1 status: approved -created_at: 2026-05-22 -updated_at: 2026-05-22 +created_at: 2026-06-03 +updated_at: 2026-06-03 authors: - "agent:analyst" --- -# BRD — ET-006: Загрузка и визуализация GPX-треков +# BRD — ET-006: Скачивание трека из popup на карте ## 1. Цель -Дать пользователю возможность загрузить GPX-файл с треком и увидеть его на карте: линию маршрута, waypoints, профиль высот и статистику. Это позволяет визуально оценить чужой или ранее записанный трек перед поездкой. +Дать пользователю возможность сохранить публичный GPS-трек (источники +OSM, EnduroRussia, Wikiloc, ttrails — слой ET-008) к себе на устройство +в виде GPX-файла прямо из popup, который открывается по клику на трек +на карте. Это закрывает базовый use case «увидел чужой интересный +трек → забрал к себе → запланировал поездку». ## 2. Контекст -- Приложение уже умеет строить маршруты через OSRM и экспортировать их в GPX (кнопка «Скачать GPX» в sheet-route). -- Обратная операция — импорт GPX — отсутствует. -- Фаза PH-3 (Smart Route) в roadmap включает работу с GPX. -- Фронтенд: MapLibre GL JS + vanilla JS, без фреймворков. -- Backend-изменения не требуются — парсинг GPX происходит на клиенте. +- Слой публичных GPS-треков реализован в ET-008 (`src/web/gps_tracks.js`, + `src/api/gps_tracks/`). При клике на трек открывается MapLibre popup + с метаданными (имя, активность, длина, дата, пользователь, источники). +- Геометрия треков хранится на сервере в SQLite: 2D WKB LineString + (`tracks.geom`, см. `src/api/gps_tracks/db.py` и `mvt.py`). Высот + по точкам в БД нет. +- На клиенте геометрия доступна только при zoom ≥ 12 (режим GeoJSON + через `GET /api/gps-tracks?bbox=...`). При zoom 8–11 popup + открывается над MVT-фичей, у которой геометрия по тайлу + упрощена/обрезана и непригодна для повторного экспорта. +- Существующий GPX в проекте умеет только парсить локальный файл и + отдаёт OSRM-маршрут (кнопка «Скачать GPX» в `sheet-route`). Экспорт + публичного трека из БД в GPX отсутствует. +- Backend — FastAPI; новый эндпоинт добавляется в существующий + router `/api/gps-tracks` (`src/api/gps_tracks/endpoint.py`). ## 3. Scope ### In scope -| # | Функция | -|---|---------| -| F-01 | Кнопка загрузки GPX в тулбаре карты | -| F-02 | Парсинг GPX 1.1 на клиенте (XML → GeoJSON) | -| F-03 | Поддержка нескольких треков в одном файле | -| F-04 | Отрисовка линии трека на карте (каждый трек — свой цвет) | -| F-05 | Отображение waypoints из GPX как маркеров с именами | -| F-06 | Fit bounds — карта центрируется на загруженном треке | -| F-07 | Загрузка нескольких файлов (треки накапливаются) | -| F-08 | Удаление отдельного трека | -| F-09 | Панель управления треками (список, цвет, удаление) | -| F-10 | Профиль высот выбранного трека | -| F-11 | Статистика трека: длина, набор высоты, сброс высоты, мин/макс высота | -| F-12 | Лимит размера файла: 50 МБ | -| F-13 | Сохранение GPX-слоёв при переключении стиля карты (тёмная тема / рельеф) | +| # | Функция | +|------|---------| +| F-01 | Кнопка/ссылка «Скачать GPX» в popup'е публичного трека (`_renderTrackPopupHtml` в `gps_tracks.js`) | +| F-02 | Backend-эндпоинт `GET /api/gps-tracks/{id}.gpx` — возвращает корректный GPX 1.1 для трека по его БД-id | +| F-03 | Имя файла: `.gpx` если есть имя, иначе `track-{id}.gpx`; передаётся в `Content-Disposition: attachment; filename=...` | +| F-04 | Тело GPX: `` с `` (name, desc, time, link на первый external_url) и одним `` с одним ``, заполненным точками из `tracks.geom` | +| F-05 | MIME-тип ответа: `application/gpx+xml; charset=utf-8` | +| F-06 | Работа единообразна на обоих zoom-режимах: при MVT (z 8–11) и при GeoJSON (z ≥ 12) popup использует один и тот же id трека и один и тот же URL — клиент НЕ собирает GPX из MVT-геометрии | +| F-07 | Ошибки: 404 для несуществующего id, 500 — через стандартный HTTPException; на клиенте: ошибка качания → toast «Не удалось скачать трек», popup не закрывается | +| F-08 | Атрибуция источников сохранена в ``: `` и/или `` содержат source_id'ы и внешние ссылки (ODbL/Wikiloc TOS — обязательно) | +| F-09 | Мобильный UX: кнопка тапабельна (≥ 36 px высота), не ломает раскладку popup'а на узких экранах | +| F-10 | Аналитика клика: событие в консоль через существующий механизм `console.log` / `showToast` (телеметрия не вводится) | ### Out of scope -- Сохранение треков на сервер / в БД -- Редактирование трека (обрезка, склейка) -- Конвертация из других форматов (KML, FIT, TCX) -- Упрощение (simplify) точек трека -- Экспорт загруженного трека обратно в GPX -- Роутинг по загруженному треку (snap to road) +- Скачивание сразу нескольких треков (batch). +- Конвертация в KML / TCX / FIT / KMZ. +- Шаринг ссылки на скачивание (короткая ссылка / OG-карточка). +- Сохранение в личную «библиотеку» пользователя (нет аккаунтов). +- Загрузка скачанного файла обратно на карту — уже покрыто текущим + `gpx.js` (UI «Загрузить GPX»), и пересечения функциональностей нет. +- Авторизация / rate-limit на загрузку — публичные данные, тот же + cors-разрешённый эндпоинт что и GeoJSON. +- Восстановление высот по DEM для GPX (трек не имеет ``). +- Изменение схемы БД `gps_tracks.sqlite`. ## 4. Метрики успеха | Метрика | Критерий | |---------|----------| -| Загрузка файла | Файл до 50 МБ загружается и парсится без ошибок за ≤ 3 сек (на среднем устройстве) | -| Визуализация | Трек отображается на карте как цветная линия | -| Waypoints | Маркеры с именами видны на карте | -| Fit bounds | Карта автоматически подстраивает zoom/center под трек | -| Множественные треки | 5+ треков отображаются одновременно, различимы по цвету | -| Удаление | Удалённый трек исчезает с карты и из панели | -| Профиль высот | Отображается корректный график высот для выбранного трека | -| Статистика | Длина, набор/сброс высоты отображаются корректно | -| Сохранение при смене стиля | GPX-треки остаются на карте после переключения тёмной темы / слоёв рельефа | -| Не ломает существующий функционал | Роутинг, рельеф, POI, линейка работают как прежде | +| Доступность кнопки | Кнопка «Скачать GPX» видна в каждом popup'е публичного трека на desktop и mobile, на обоих zoom-режимах (z = 9 и z = 14) | +| Корректность файла | Скачанный файл валидируется парсером `gpx.js` (drag-and-drop в приложение) и рисуется на карте без ошибок | +| Корректность GPX 1.1 | Файл проходит XSD-валидацию `http://www.topografix.com/GPX/1/1/gpx.xsd` (online valdiator или `xmllint --schema`) | +| Совместимость | Файл открывается в OsmAnd / Locus / `gpx.studio` без ошибок | +| Имя файла | Имя файла читаемое, не содержит запрещённых на Windows/Android символов (`/\:*?"<>|`) | +| Атрибуция | В `` явно перечислены все source_id и external_urls трека | +| Производительность | Эндпоинт отвечает ≤ 500 ms p95 для трека до 50 000 точек (среднестатистический эндуро-трек < 5000 точек) | +| Размер ответа | Для трека 5000 точек тело GPX ≤ 600 КБ (без gzip), ≤ 150 КБ (с gzip) | +| Регрессии | Существующий popup, MVT-/GeoJSON-режимы слоя, фильтры активностей/источников, halo на спутнике — не сломаны | ## 5. Риски | Риск | Вероятность | Влияние | Митигация | |------|-------------|---------|-----------| -| Большой GPX (50 МБ, 500K+ точек) тормозит рендеринг | Средняя | Среднее | Использовать GeoJSON source + line layer (MapLibre оптимизирует); при необходимости — Web Worker для парсинга | -| GPX без данных высот → профиль пустой | Средняя | Низкое | Показать сообщение «Данные высот отсутствуют» | -| Невалидный GPX → ошибка парсинга | Низкая | Низкое | Показать пользователю понятное сообщение об ошибке | -| Конфликт цветов треков с цветами маршрута OSRM | Низкая | Низкое | Использовать отдельную палитру, отличную от цветов роутинга | +| Геометрия трека отсутствует (`row.geom is None`) | Низкая | Низкое | 404 + toast «Геометрия трека недоступна»; такие записи редки (защита уже в `_wkb_to_coords`) | +| Очень длинный трек (десятки тысяч точек) → ответ > 5 МБ | Низкая | Среднее | Без упрощения геометрии. Документировать лимит. При необходимости — gzip через nginx (уже включён) | +| Wikiloc TOS требует ссылки на оригинал | Высокая | Высокое | F-08: `` на оригинальную страницу для каждого external_url | +| ODbL атрибуция в GPX | Высокая | Высокое | F-08: `` для треков с `source = osm` | +| Опасный символ в `name` → XML-инъекция | Низкая | Высокое | Эскейп через стандартный XML-сериализатор (ElementTree / минимальный helper) | +| Имя файла с кириллицей не сохраняется в Safari | Средняя | Низкое | RFC 5987 — `filename*=UTF-8''` + ASCII-fallback `filename=...` | +| Кнопка обрезается на узком экране (≤ 360 px) | Средняя | Низкое | Кнопка на отдельной строке, `flex-wrap`, тестируется в UI-тестах TC-UI-02 | ## 6. Зависимости -- Нет внешних зависимостей -- Только фронтенд (vanilla JS + MapLibre GL JS) -- Парсинг XML: нативный DOMParser браузера +- Бэкенд: существующий router `/api/gps-tracks` + (`src/api/gps_tracks/endpoint.py`), функции `_wkb_to_coords` и БД-слой. + Изменения чисто аддитивные. +- Фронтенд: модуль `src/web/gps_tracks.js`, функция + `_renderTrackPopupHtml`. Точечная правка HTML-шаблона. +- Никаких внешних сервисов / новых данных / миграций БД. +- Никаких новых npm/pip-зависимостей: GPX генерируется штатным + `xml.etree.ElementTree` (есть в stdlib Python 3.12). diff --git a/docs/work-items/ET-006/02-trz.md b/docs/work-items/ET-006/02-trz.md index 44ba8e1..a3a7fb9 100644 --- a/docs/work-items/ET-006/02-trz.md +++ b/docs/work-items/ET-006/02-trz.md @@ -1,289 +1,295 @@ --- type: trz work_item_id: ET-006 -title: "ТЗ: Загрузка и визуализация GPX-треков" -version: 2 +title: "ТЗ: Скачивание трека из popup на карте" +version: 1 status: approved -created_at: 2026-05-22 -updated_at: 2026-05-22 +created_at: 2026-06-03 +updated_at: 2026-06-03 authors: - "agent:analyst" --- -# ТЗ — ET-006: Загрузка и визуализация GPX-треков +# ТЗ — ET-006: Скачивание трека из popup на карте -## 1. Функциональные требования +> Документ описывает требования к функциональности. Архитектурные +> решения (выбор XML-библиотеки, формат потоковой генерации и т. п.) +> остаются на этап Architect → ADR. -### REQ-F-01: Кнопка загрузки GPX +## 1. Сводка -- В правой панели кнопок карты (`#map-controls-r`) добавляется кнопка «GPX» с иконкой загрузки (стрелка вверх + документ). -- Позиция: между кнопкой «Компас» и «Моё местоположение» (верхняя часть панели). -- По нажатию открывается системный диалог выбора файла (``). -- Допускается множественный выбор (`multiple`). +Добавить кнопку «Скачать GPX» в popup публичного GPS-трека (слой +ET-008, `gps_tracks.js`). По нажатию браузер инициирует загрузку +файла GPX 1.1 c геометрией и метаданными трека, который сервер +формирует на лету по новому HTTP-эндпоинту +`GET /api/gps-tracks/{id}.gpx`. -### REQ-F-02: Парсинг GPX +## 2. Затрагиваемые компоненты -- Парсинг выполняется на клиенте через `DOMParser` (XML → DOM → GeoJSON). -- Поддерживается GPX 1.1 (namespace `http://www.topografix.com/GPX/1/1`). -- Извлекаются: - - `` → массив треков, каждый `` → массив точек `[lon, lat, ele?, time?]` - - `` → waypoints `{lon, lat, name?, ele?}` - - `` → route points (трактуются как трек) -- Если файл содержит несколько ``, каждый трек — отдельная сущность. -- При ошибке парсинга — показать toast-уведомление: «Не удалось прочитать GPX-файл». +| Слой | Файл / модуль | Тип правки | +|---------|--------------|------------| +| Backend | `src/api/gps_tracks/endpoint.py` | новый handler `get_track_gpx` | +| Backend | `src/api/gps_tracks/db.py` (или новый модуль) | новый helper `get_track_by_id(conn, id)` | +| Backend | `src/api/gps_tracks/gpx_builder.py` (новый) | сериализация WKB → GPX 1.1 XML | +| Frontend | `src/web/gps_tracks.js` | дополнение `_renderTrackPopupHtml` (кнопка) | +| Frontend | `src/web/app.css` | стили для `.track-popup-download` | +| Tests | `tests/unit/test_gpx_builder.py` (новый) | unit GPX-сериализатора | +| Tests | `tests/integration/test_gps_tracks_endpoint.py` (расширить) | новый case на `.gpx` endpoint | +| Tests | `tests/e2e/test_track_popup_download.spec.js` (новый) | Playwright e2e | -### REQ-F-03: Валидация +## 3. Функциональные требования -- Максимальный размер файла: 50 МБ. При превышении — toast: «Файл слишком большой (макс. 50 МБ)». -- Если файл не содержит ни одного трека и ни одного waypoint — toast: «GPX-файл не содержит данных». +### 3.1 Backend: GET /api/gps-tracks/{id}.gpx -### REQ-F-04: Отрисовка трека на карте +**REQ-F-01.** Эндпоинт `GET /api/gps-tracks/{track_id}.gpx` +регистрируется в существующем router'е (`prefix="/api/gps-tracks"`), +без auth. -- Каждый трек отрисовывается как `line` layer в MapLibre. -- Source: GeoJSON (`LineString` или `MultiLineString`). -- Цвет: из палитры 8 цветов, циклически. Палитра отличается от цветов роутинга (синий/зелёный/оранжевый). - - Предлагаемая палитра: `#e6194b`, `#3cb44b`, `#ffe119`, `#4363d8`, `#f58231`, `#911eb4`, `#42d4f4`, `#f032e6`. -- Толщина линии: 4px. -- Opacity: 0.85. -- Z-index: выше базовых слоёв, ниже маршрута OSRM (если активен). +**REQ-F-02.** Параметр `track_id: int` валидируется FastAPI как `int`. +Не-int → 422 (стандартное поведение FastAPI). -### REQ-F-05: Отображение waypoints +**REQ-F-03.** Если трек с таким id не найден или `tracks.geom` пустой / +WKB не парсится — ответ `404 Not Found`, тело +`{"detail":"Track not found"}`. -- Каждый `` отображается как маркер (circle layer + symbol layer для имени). -- Цвет маркера: совпадает с цветом трека из того же файла (или нейтральный, если waypoints без трека). -- Имя waypoint (``) отображается как label рядом с маркером. -- Если имя отсутствует — маркер без подписи. +**REQ-F-04.** Успешный ответ: + - `Content-Type: application/gpx+xml; charset=utf-8` + - `Content-Disposition: attachment; filename=""; filename*=UTF-8''` + - `Access-Control-Allow-Origin: *` (как у соседних эндпоинтов). + - Тело — валидный GPX 1.1 XML. -### REQ-F-06: Fit bounds +**REQ-F-05.** Имя файла: + - Базовое имя = `track.name`, если есть и не пустое; иначе `track-{id}`. + - Удалить запрещённые символы Windows/Android: `/\:*?"<>|` и + управляющие < 0x20. + - Свернуть пробелы, обрезать до 64 символов. + - Расширение `.gpx`. + - ASCII-fallback: транслит/удаление не-ASCII, минимум `track-{id}.gpx`. -- После загрузки файла карта выполняет `fitBounds` по bbox всех точек загруженного файла. -- Padding: 50px со всех сторон. -- Если загружено несколько файлов подряд — fit bounds только по последнему загруженному. +### 3.2 Структура GPX 1.1 -### REQ-F-07: Множественная загрузка - -- Треки из разных файлов накапливаются в сессии. -- Каждый файл получает следующий цвет из палитры. -- Максимальное количество одновременных треков: не ограничено (разумный предел — производительность браузера). - -### REQ-F-08: Удаление трека - -- В панели управления треками (REQ-F-09) у каждого трека есть кнопка удаления (иконка ✕). -- При удалении: убирается line layer, source, маркеры waypoints с карты. -- Если удалён активный (выбранный) трек — панель профиля высот скрывается. - -### REQ-F-09: Панель управления треками (GPX Sheet) - -- Реализуется как bottom sheet (`#sheet-gpx`), аналогично существующим sheet-route, sheet-recon. -- Открывается автоматически при загрузке первого трека. -- Содержит: - - Заголовок «GPX-треки» с иконкой и кнопкой свернуть. - - Список загруженных треков: цветной кружок + имя файла (без расширения) + кнопка удаления. - - По тапу на трек в списке — он становится «активным» (выделяется), показывается его статистика и профиль высот. -- Кнопка в тулбаре нижнего toolbar (`#toolbar`): «GPX» — переключает видимость sheet. - -### REQ-F-10: Профиль высот - -- Отображается в нижней части sheet-gpx (под списком треков) для активного трека. -- График: canvas-элемент, ширина 100% sheet, высота 120px. -- Ось X: расстояние от начала трека (км). -- Ось Y: высота (м). -- Линия графика: цвет трека. -- Заливка под линией: цвет трека с opacity 0.2. -- Если данные высот отсутствуют (`` нет) — показать текст: «Данные высот отсутствуют». -- При наведении/тапе на график — показать tooltip с высотой и расстоянием, и подсветить соответствующую точку на карте (маркер-курсор). - -### REQ-F-11: Статистика трека - -- Отображается над профилем высот в sheet-gpx для активного трека. -- Формат: компактная сетка (аналогично recon-grid). -- Поля: - - Длина (км) — сумма расстояний между точками (Haversine). - - Набор высоты (м) — сумма положительных дельт `ele`. - - Сброс высоты (м) — сумма отрицательных дельт `ele` (абсолютное значение). - - Мин. высота (м). - - Макс. высота (м). -- Если данные высот отсутствуют — показать только длину, остальные поля: «—». - -### REQ-F-12: Интерактивность трека на карте - -- При клике на линию трека на карте — этот трек становится активным в панели (показывается статистика + профиль). -- Курсор при наведении на трек: pointer. - -### REQ-F-13: Сохранение треков при переключении стиля карты - -- При переключении стиля карты (тёмная тема, восстановление слоёв рельефа) вызывается `map.setStyle()`, который удаляет **все** пользовательские source и layer. -- После смены стиля все загруженные GPX-треки должны быть автоматически восстановлены: линии треков, source, waypoints-маркеры. -- Восстановление выполняется в функции `rebuildMapOverlays()` (`src/web/app.js`) — по аналогии с уже реализованными там маршрутом OSRM, разведкой и scenic-маршрутами. -- Данные треков (`window.gpxTracks`) хранятся в памяти и при `setStyle()` не теряются — пересоздаются только объекты карты (source / layer / маркеры). -- Активный трек, его статистика и профиль высот должны сохраняться после переключения стиля. -- Z-order GPX-слоёв (см. REQ-F-04) корректно восстанавливается и после смены стиля. - -## 2. Нефункциональные требования - -### REQ-NF-01: Производительность - -- Парсинг файла 50 МБ: ≤ 5 секунд на устройстве с 4 ГБ RAM. -- Рендеринг трека 500K точек: без видимых фризов при pan/zoom (MapLibre оптимизирует GeoJSON line layers). -- Во время парсинга показывать индикатор загрузки (spinner или moto-wheel). - -### REQ-NF-02: Совместимость - -- Работает в Chrome 90+, Firefox 90+, Safari 15+. -- Работает на мобильных (touch events для профиля высот). - -### REQ-NF-03: UX - -- Кнопка загрузки доступна всегда, независимо от активного режима (роутинг, разведка и т.д.). -- GPX-треки не конфликтуют с активным маршрутом OSRM — отображаются одновременно. -- При ошибках — toast-уведомления (не alert/confirm). - -### REQ-NF-04: Хранение - -- Данные треков хранятся только в памяти (JS-переменные). -- При перезагрузке страницы — все треки теряются. -- Не используется localStorage/sessionStorage для данных треков (слишком большие). - -## 3. UI-спецификация - -### 3.1 Кнопка в правой панели (#map-controls-r) - -``` -┌──────────┐ -│ ↑ GPX │ ← новая кнопка (между Компас и Геолокация) -└──────────┘ -``` - -- Класс: `map-btn` -- ID: `btn-gpx-upload` -- Иконка: стрелка вверх из документа (upload file) -- Title: «Загрузить GPX» - -### 3.2 Кнопка в нижнем тулбаре (#toolbar) - -``` -[ Маршрут | Связка | Красивый | Разведка | Линейка | Поиск | Метка | GPX ] -``` - -- Класс: `tb-btn` -- ID: `tb-gpx` -- Иконка: файл с линией (track) -- Label: «GPX» -- Действие: `toggleGpxSheet()` - -### 3.3 Bottom sheet (#sheet-gpx) - -``` -┌─────────────────────────────────────┐ -│ ═══ (handle) │ -│ 📄 GPX-треки [свернуть]│ -├─────────────────────────────────────┤ -│ 🔴 track_morning.gpx [✕] │ -│ 🔵 weekend_ride.gpx ✓ [✕] │ ← активный (выделен) -│ 🟢 test_route.gpx [✕] │ -├─────────────────────────────────────┤ -│ СТАТИСТИКА │ -│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ -│ │47км│ │820м│ │650м│ │120м│ │980м│ │ -│ │длин│ │наб.│ │сбр.│ │мин │ │макс│ │ -│ └────┘ └────┘ └────┘ └────┘ └────┘ │ -├─────────────────────────────────────┤ -│ ПРОФИЛЬ ВЫСОТ │ -│ ┌───────────────────────────────┐ │ -│ │ ╱╲ ╱╲╱╲ │ │ -│ │╱ ╲╱╲╱ ╲╱╲ │ │ -│ └───────────────────────────────┘ │ -│ 0 km 23.5 km 47 km │ -└─────────────────────────────────────┘ -``` - -### 3.4 Toast-уведомления - -- Позиция: верх экрана, по центру. -- Автоскрытие: 4 секунды. -- Стиль: аналогично `#ruler-toast`. - -## 4. Данные - -### Входные данные (GPX 1.1) +**REQ-F-06.** Корень: ```xml - - - Morning Ride - - 150 - ... - - - - Кафе - 155 - + + ... ``` -### Внутренняя модель (JS) +**REQ-F-07.** `` обязательно содержит: + - `` = имя трека (или `track-{id}`). + - `` = склейка из `track.description` (если есть), активности + (русское `GPS_ACTIVITY_LABELS[activity]`), длины (км), источников + (список source_id через запятую). Многострочно через `\n`. + - `