Files
enduro-trails/docs/work-items/ET-011/02-trz.md
claude-bot 2bf08a10e3
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Successful in 6s
CI / build (push) Has been skipped
analyst(ET): auto-commit from analyst run_id=63
2026-06-03 20:05:12 +00:00

13 KiB
Raw Blame History

ТЗ: Скачивание трека из popup на карте

Work Item: ET-011 Стадия: Анализ → Architecture Автор: analyst Дата: 2026-06-03


1. Сводка

Добавить в существующий popup публичного GPS-трека (слой ET-008) кнопку «Скачать», которая запрашивает с сервера GPX-файл и сохраняет его в загрузки пользователя. Новый backend-эндпоинт собирает GPX 1.1 из геометрии трека в БД gps_tracks.sqlite.

2. Функциональные требования

REQ-F-01 — Кнопка «Скачать» в popup трека

В popup публичного трека (создаётся в _renderTrackPopupHtml(props), src/web/gps_tracks.js, l.463) должна появляться кнопка «Скачать».

  • Иконка: download (SVG, как в sheet-route downloadGPX, l.135137 в index.html).
  • Tooltip / aria-label: «Скачать GPX».
  • Размещение: в правом верхнем углу popup, рядом с названием трека, или отдельной строкой в конце popup перед источниками — на усмотрение архитектора, но всегда видна без скролла.
  • Тапабельная зона: ≥ 32×32 CSS px (mobile-friendly, REQ-NF-04 ниже).

REQ-F-02 — Backend: эндпоинт скачивания

Реализовать в роутере src/api/gps_tracks/endpoint.py новый GET-эндпоинт:

GET /api/gps-tracks/{track_id}/download
GET /api/gps-tracks/{track_id}/download?format=gpx   (синоним)

Параметры:

  • track_id (path, int, обязательный) — tracks.id из БД.
  • format (query, optional, default=gpx) — формат файла. Допустимые значения для текущей итерации: gpx. (При закрытии Q-2 = «делаем KML» — добавится kml.)

Поведение:

  • 200 + Content-Type: application/gpx+xml (для GPX) или application/vnd.google-earth.kml+xml (для KML).
  • Content-Disposition: attachment; filename="<safe-name>.gpx"; filename*=UTF-8''<urlencoded-name>.gpx (RFC 5987, REQ-NF-05 ниже).
  • 404, если track_id не существует.
  • 400, если format не входит в whitelist.
  • 403, если источник трека запрещает реэкспорт (см. REQ-F-06 и Q-1 в BRD).

REQ-F-03 — Содержимое GPX

GPX-файл должен соответствовать схеме GPX 1.1 (http://www.topografix.com/GPX/1/1) и содержать:

  • Корневой <gpx> с атрибутами:
    • version="1.1"
    • creator="Enduro Trails"
    • xmlns="http://www.topografix.com/GPX/1/1"
    • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    • xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
  • Блок <metadata> с:
    • <name>tracks.name или «Без названия».
    • <desc>tracks.description (если есть).
    • <time>tracks.created_at в ISO-8601 (если есть, иначе пропустить).
    • <author><name>tracks.user (если есть).
    • <link href="<external_url>"><text>Источник: <source_id></text></link> — по одному <link> на каждый элемент external_urls.
    • <copyright author="Enduro Trails"><license>https://www.openstreetmap.org/copyright</license></copyright> — для OSM-источника. Для других — без <copyright> либо со ссылкой на исходный URL.
  • Ровно один <trk> с:
    • <name>tracks.name.
    • <type>activity_type (например, enduro).
    • Ровно один <trkseg> с <trkpt lat="..." lon="..."> для каждой координаты из WKB-геометрии tracks.geom. Без <ele> и <time> (см. BRD A2).

REQ-F-04 — Имя файла

Имя файла (для Content-Disposition и filename*) формируется так:

  1. Берём tracks.name. Если пустое / NULL — используем track-<id>.
  2. Заменяем все недопустимые для FAT/NTFS символы (/ \ : * ? " < > |) на _.
  3. Триммим до 80 символов.
  4. Транслитерация не нужна — современные браузеры понимают filename*=UTF-8''… (RFC 5987).
  5. Расширение: .gpx (или .kml).

Например: tracks.name = "По грязи к Чёрному озеру"По грязи к Чёрному озеру.gpx (через filename*=UTF-8''%D0%9F%D0%BE…).

REQ-F-05 — Поведение на фронте

При клике на кнопку «Скачать»:

  1. Не закрывать popup (или закрывать — на усмотрение архитектора, главное консистентно с остальными кнопками в проекте). Рекомендация: не закрывать, чтобы пользователь видел индикатор/успех.
  2. Сделать GET-запрос на /api/gps-tracks/{id}/download через <a href="..." download="...">.click() (стандартный паттерн, отлично работает в desktop и mobile-браузерах) или через fetch + Blob
    • URL.createObjectURL — выбор за архитектором, см. R-1 в BRD.
  3. На время запроса показать спиннер/индикатор на самой кнопке (опц.) — нужно если бэк > 200 ms. Hint: трек на 50 000 точек собирается ≈ 80150 ms (см. NFR-01), так что индикатор большинству не нужен.
  4. При ошибке (HTTP ≠ 200) — показать showToast(...) (функция уже есть в проекте) с человекочитаемым сообщением:
    • 403 → «Источник запрещает скачивание. Откройте трек на сайте источника.»
    • 404 → «Трек не найден.»
    • 5xx / network → «Не удалось скачать. Попробуйте ещё раз.»

REQ-F-06 — Защита по лицензии источника (зависит от Q-1)

Если Owner закрывает Q-1 как «только OSM»:

  • Backend проверяет tracks.sources_json. Если ни одного из источников не относится к разрешённому whitelist'у (по умолчанию ["osm"]) — возвращает 403 c JSON {"detail":"source_forbidden", "external_urls":[...]}.
  • Frontend в обработчике 403 показывает toast и, если есть external_urls, кнопку «Открыть на сайте источника».

Если Owner отвечает «всё разрешено» — этот REQ становится no-op (вырезать).

REQ-F-07 — Логирование

Каждое успешное скачивание логируется server-side: uvicorn access-log + (опц.) отдельная строка в stdout формата track_download id=<id> source=<sources> size_bytes=<n> ip=<remote>. Это нужно для NFR-06 (наблюдаемость).

3. Нефункциональные требования

REQ-NF-01 — Производительность

Сборка GPX и отдача для трека до 50 000 точек — не дольше 300 ms от запроса до начала ответа (P95 на текущем железе test-среды). Размер ответа для типичного трека 100 км / 5 000 точек — до 800 КБ (чистый XML, без gzip; ответ может быть gzip'нут средствами uvicorn).

REQ-NF-02 — Потолок размера ответа

Если число точек в треке > 200 000 (защита от patho-кейсов) — возвращать 413 Payload Too Large с сообщением «Трек слишком большой для скачивания». Реализация: проверка tracks.points_count до сборки XML.

REQ-NF-03 — Соответствие схеме GPX 1.1

Полученный файл должен проходить валидацию по схеме http://www.topografix.com/GPX/1/1/gpx.xsd без warnings/errors. Тест в tests/api/test_gps_tracks_download.py (см. test plan).

REQ-NF-04 — UX mobile

  • Кнопка «Скачать» должна быть удобно тапабельной на мобильных (≥ 32×32 CSS px).
  • Popup не должен «прыгать» из-за появления кнопки — высота фиксирована или растёт плавно.
  • При ширине viewport < 420 px кнопка остаётся видимой (popup имеет max-width: 300px — см. gps_tracks.js l.514).

REQ-NF-05 — Заголовок Content-Disposition

Заголовок должен поддерживать UTF-8 имена через RFC 5987:

Content-Disposition: attachment; filename="track.gpx"; filename*=UTF-8''%D0%9F%D0%BE…

Параметр filename (без *) — ASCII-fallback (транслит или track-<id>.gpx).

REQ-NF-06 — Наблюдаемость

  • 200/4xx/5xx ответы видны в uvicorn access-log.
  • Стек-трейсы 5xx уходят в stderr (текущая практика FastAPI/uvicorn).
  • Метрики (RPS / latency) — не требуются в этой итерации.

REQ-NF-07 — Безопасность

  • track_id — int, парсится FastAPI, защита от SQL-инjection встроенная.
  • Имя файла санитизуется (REQ-F-04) — защита от path-traversal в загрузках.
  • Access-Control-Allow-Origin: * уже стоит в CORS middleware — не трогаем; iframe-embed возможен.

4. Out of scope (явно)

  • KML — в backlog (см. Q-2). Если Owner закрывает Q-2 как «делаем» — REQ-F-02 расширяется (format=kml), но это не предмет данной итерации.
  • Сохранение скачанного трека в IndexedDB / в sheet-gpx (как пользовательский GPX по ET-006) — отдельная фича.
  • Bulk-download (несколько треков). Только один за запрос.
  • Конвертация формата (waypoints, маркеры).

5. Артефакты, к которым прикасаемся

  • src/web/gps_tracks.js — функция _renderTrackPopupHtml(props) и (вероятно) обработчик клика на новую кнопку.
  • src/web/app.css (или gps_tracks.js inline-стили) — стиль кнопки.
  • src/api/gps_tracks/endpoint.py — добавляется новый route.
  • src/api/gps_tracks/db.py (возможно) — функция get_track_by_id().
  • tests/api/test_gps_tracks_download.py — новые тесты (см. test plan).
  • tests/web/test_gps_tracks_popup.spec.ts или аналог — UI-тесты (Playwright, см. 04b-ui-test-cases.md).
  • ADR docs/work-items/ET-011/06-adr/*.md (создаст architect): про механизм отдачи (link vs blob), про обработку лицензии источника.

6. Зависимости

  • Слой ET-008 «Публичные треки» уже в проде (тестовая среда). Этот work item расширяет его popup.
  • БД gps_tracks.sqlite инициализируется через миграцию migrations/gps_tracks_001_init.sql — её менять не нужно (все необходимые поля уже есть: id, name, description, activity_type, user, created_at, length_m, points_count, geom, sources_json, external_urls_json).

7. Глоссарий

  • Public track — публичный GPS-трек из таблицы tracks в БД gps_tracks.sqlite. Источник — OSM, EnduroRussia, Wikiloc, ttrails и т.п.
  • GPX — GPS Exchange Format 1.1, XML-формат для треков и точек.
  • KML — Keyhole Markup Language 2.2, XML-формат Google Earth.
  • Popup — MapLibre maplibregl.Popup, всплывающее окно по клику на feature.