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

235 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ: Скачивание трека из 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.