# ТЗ: Скачивание трека из 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.135–137 в
  `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 точек собирается
   ≈ 80–150 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.
