diff --git a/docs/work-items/ET-011/01-brd.md b/docs/work-items/ET-011/01-brd.md new file mode 100644 index 0000000..3605692 --- /dev/null +++ b/docs/work-items/ET-011/01-brd.md @@ -0,0 +1,162 @@ +# BRD: Скачивание трека из popup на карте (ET-011) + +Work Item ID: ET-011 +Phase: PH-3.smart-route (расширение публичных треков из PH-3/PH-5) +Stage: analysis +Author: analyst-bot +Date: 2026-06-03 + +## 1. Краткое описание + +Пользователь видит на карте публичные GPS-треки (фича ET-008). Сейчас при тапе +по треку открывается popup с метаданными (название, активность, длина, дата, +источник). Возможности скачать трек к себе на устройство нет — приходится идти +во внешний источник по ссылке, который может быть недоступен / требовать +регистрации. + +В этом WI добавляем кнопку «Скачать» прямо в popup трека. По нажатию +браузер скачивает файл трека в формате GPX. Опционально — выбор формата GPX/KML. + +## 2. Цели и метрики + +### Бизнес-цели + +- **G1.** Дать пользователю возможность забрать понравившийся трек в свой + GPS-навигатор / телефон без ухода со страницы и без авторизации на внешних + сервисах. +- **G2.** Снизить число «отказов» (close popup без действия): сейчас единственное + доступное действие — переход по внешней ссылке. +- **G3.** Подготовить инфраструктуру отдачи трека для будущих фич (импорт + публичного трека в свой список GPX-треков — ET-006 backlog). + +### Продуктовые метрики (post-launch) + +- M1. Доля сессий с хотя бы одним кликом «Скачать» среди сессий, открывших + popup публичного трека: целевое значение ≥ 25%. +- M2. CTR кнопки «Скачать» в popup: ≥ 40% (от всех открытий popup). +- M3. 95-й перцентиль времени отдачи файла трека: < 800 мс для треков + ≤ 5000 точек. +- M4. Доля 5xx ошибок эндпоинта скачивания: < 0.5%. + +## 3. Аудитория и сценарии + +### Персоны + +- **P1 — Эндуро-райдер на десктопе**, планирует выезд: листает публичные треки, + отбирает интересные, скачивает GPX, грузит в свой Garmin / TwoNav / OsmAnd. +- **P2 — Эндуро-райдер с телефона перед выездом**: открыл карту на смартфоне, + тапнул трек, нажал «Скачать», файл уехал в загрузки телефона, передан в OsmAnd + через «Открыть с помощью». + +### Основные сценарии + +#### UC-1. Скачивание GPX с десктопа + +1. Пользователь включил слой «Публичные треки» на карте. +2. Кликнул по линии трека. +3. Открылся popup с метаданными трека и кнопкой «Скачать GPX». +4. Пользователь нажал кнопку. +5. Браузер начал загрузку файла `<имя_трека>.gpx`. +6. Popup остаётся открытым (можно скачать другой формат или закрыть вручную). + +#### UC-2. Скачивание с мобильного + +1. То же, но тап по треку на мобильном. +2. Popup открыт; кнопка «Скачать» доступна с минимальной hit-зоной 44×44 px. +3. По тапу запускается стандартное сохранение файла в Downloads / Files iOS. + +#### UC-3. Выбор формата GPX vs KML (опционально, может уехать в backlog) + +1. В popup кнопка «Скачать» с выпадающим выбором формата (GPX / KML). +2. По умолчанию — GPX. +3. KML включён только если фича-флаг `featureKmlExport=true` (см. §6). + +#### UC-4. Ошибочные ситуации + +- Трек не найден (удалён из БД между моментом отображения и кликом) → + тост-уведомление «Трек больше недоступен», popup закрывается. +- Бэкенд недоступен / 5xx → тост «Не удалось скачать трек, попробуйте позже», + popup остаётся. + +## 4. Скоуп + +### In scope + +- Кнопка «Скачать» в существующем popup публичных GPS-треков (см. `gps_tracks.js` + `_renderTrackPopupHtml`). +- Backend-эндпоинт отдачи трека в формате GPX по `id` из БД `gps_tracks.sqlite`. +- Корректный `Content-Disposition: attachment; filename="..."` с безопасным + именем файла. +- UI-обратная связь (тост при ошибке, минимальный визуальный feedback при + старте загрузки). +- Логи / X-Cache-style заголовок для observability. + +### Out of scope + +- Скачивание GPX-треков, загруженных пользователем локально (фича ET-006 — у + них уже есть локальный экспорт через `downloadGPX()` в app.js, отдельный UX). +- Скачивание построенных маршрутов из «Маршрут / Красивый / Связка» — отдельный + WI (часть UI у них уже есть, см. `sheet-icon-btn onclick=downloadGPX()`). +- Массовая выгрузка треков (Bulk download) — backlog. +- Авторизация / лимиты по IP — отдельный security WI. + +### Tentative (под решение в TRZ) + +- **KML экспорт**. Технически тривиально, но добавляет UI-усложнение (dropdown + вместо одной кнопки). Решение принимается в TRZ. Дефолт: GPX обязателен, + KML — фича-флаг. + +## 5. Бизнес-требования (что должно стать возможным) + +- **BR-1.** Пользователь, открывший popup публичного трека, видит кнопку + «Скачать» в popup. +- **BR-2.** Нажатие кнопки приводит к скачиванию файла трека на устройство + пользователя без перезагрузки страницы. +- **BR-3.** Скачанный GPX корректно открывается в OsmAnd / Garmin BaseCamp / + любом стандартном GPX-клиенте (валидный GPX 1.1). +- **BR-4.** Имя файла читаемое: содержит название трека (или дефолт + `track-.gpx`, если имени нет). +- **BR-5.** В GPX сохраняется как минимум: треклог (LineString), название + трека, и (если есть) описание + ссылка на источник в качестве комментария. +- **BR-6.** При ошибке пользователь получает понятное сообщение и popup не + ломается. + +## 6. Бизнес-флаги, ограничения + +- **Не нарушать лицензии источников.** Если трек получен из внешнего + источника (Strava heatmap, Wikiloc и т.п.), в GPX `` + должна быть ссылка на оригинал. Текст BR-5. +- **Без авторизации.** Скачивание доступно анонимно — это публичные треки. +- **Без rate-limiting в MVP**, но эндпоинт должен логировать запросы для + будущей защиты от парсинга. +- **Бюджет на UI.** Максимум +1 кнопка в popup. Без отдельных модалок, + bottom-sheet, диалогов. + +## 7. Бизнес-риски + +| ID | Риск | Митигация | +|----|------|-----------| +| BR-R1 | Источник запрещает редистрибуцию (Strava ToS) | Включить ссылку на оригинал в GPX-метаданные; явно показать в popup, что трек агрегирован, а не «наш». В TRZ определить per-source флаг разрешения скачивания. | +| BR-R2 | Большой трек (50000+ точек) → таймаут сборки GPX | Ограничение размера / упрощение (Douglas-Peucker) — решение в TRZ. | +| BR-R3 | Парсинг базы автоматизированными ботами | MVP: только логирование. Будущее: cache-busting токен / rate-limit. | +| BR-R4 | Имя трека содержит спецсимволы → битый Content-Disposition | Санитизация имени + RFC 5987 (filename\*) — детали в TRZ. | + +## 8. Зависимости + +- **ET-008** (публичные GPS-треки): эта фича расширяет уже реализованный popup. + Без ET-008 показывать кнопку негде. Если ET-008 откатывается — ET-011 + тоже не имеет смысла. +- БД `gps_tracks.sqlite` с таблицей `tracks` (схема — `migrations/gps_tracks_001_init.sql`): + поля `id`, `name`, `description`, `geom` (WKB LineString), + `sources_json`, `external_urls_json`. +- FastAPI роутер `/api/gps-tracks` (`src/api/gps_tracks/endpoint.py`). +- Frontend: `src/web/gps_tracks.js`, функция `_renderTrackPopupHtml`. + +## 9. Открытые вопросы (адресуются в TRZ) + +- Q1. KML обязателен или фича-флаг? → **Дефолт: фича-флаг, MVP без KML.** +- Q2. Включать ли в GPX waypoints / точки треков с timestamps? → треки в + БД хранятся как LineString без timestamp; включаем только координаты. +- Q3. Лимит размера трека? → жёсткий лимит 100k точек, soft warning > 20k. +- Q4. Per-source флаг «разрешена редистрибуция»? → MVP: разрешено всем; + ссылка на оригинал в GPX обязательна. Решение принимается в ADR. diff --git a/docs/work-items/ET-011/02-trz.md b/docs/work-items/ET-011/02-trz.md new file mode 100644 index 0000000..a095fd9 --- /dev/null +++ b/docs/work-items/ET-011/02-trz.md @@ -0,0 +1,362 @@ +# ТЗ (TRZ): Скачивание трека из popup на карте (ET-011) + +Work Item ID: ET-011 +Phase: PH-3.smart-route (расширение ET-008) +Stage: analysis +Author: analyst-bot +Date: 2026-06-03 + +## 1. Контекст + +В ET-008 реализован слой публичных GPS-треков с popup при клике, в котором +отображаются метаданные трека. Кнопки «Скачать» нет — пользователь может +только перейти по внешней ссылке на источник. + +Файлы / точки расширения: + +- **Frontend:** `src/web/gps_tracks.js` — функция `_renderTrackPopupHtml(props)` + (строки ~463–501) генерирует HTML popup. Обработчик клика — + `_setupGpsClickHandler(map)` (строки ~503–523), использует `maplibregl.Popup`. +- **Backend:** `src/api/gps_tracks/endpoint.py` — FastAPI APIRouter + с `prefix="/api/gps-tracks"`. Уже есть `GET ""`, `GET /tiles/{z}/{x}/{y}.mvt`, + `GET /health`, `POST /cache/clear`. +- **БД:** `gps_tracks.sqlite`, таблица `tracks`. Поля релевантные ET-011: + `id` (INTEGER PK), `name`, `description`, `created_at`, `user`, + `length_m`, `points_count`, `geom` (BLOB WKB LineString), + `sources_json`, `external_urls_json`. +- **WKB парсер:** `src/api/gps_tracks/mvt.py::_wkb_to_coords(blob)` — уже + используется в `_row_to_geojson_feature`. + +## 2. Функциональные требования + +### REQ-F-01. Backend: эндпоинт скачивания GPX + +- **Endpoint:** `GET /api/gps-tracks/{track_id}/download` +- **Параметры пути:** `track_id: int` — primary key из таблицы `tracks`. +- **Query-параметры:** + - `format: str = "gpx"` — формат файла, допустимые значения: `gpx`, `kml`. + При запросе `kml` если фича-флаг `featureKmlExport=false` (см. REQ-NF-06) — + ответ `400 Bad Request` с телом `{"detail": "Format kml is not enabled"}`. +- **Ответы:** + - `200 OK`: + - `Content-Type: application/gpx+xml` (для gpx) или + `application/vnd.google-earth.kml+xml` (для kml). + - `Content-Disposition: attachment; filename="track-.gpx"; + filename*=UTF-8''.gpx` (RFC 5987). + - `Cache-Control: public, max-age=3600` (треки иммутабельны на горизонте + часа; updated_at рассматривается отдельно). + - `Access-Control-Allow-Origin: *`. + - Тело — валидный GPX 1.1 / KML 2.2. + - `404 Not Found`: тело `{"detail": "Track not found"}`. + - `400 Bad Request`: формат не поддержан / фича-флаг выключен. + - `503 Service Unavailable`: при ошибке БД. + +### REQ-F-02. Состав GPX 1.1 + +GPX MUST содержать: + +- Объявление XML: `` +- Корневой элемент ``. +- `` с дочерними: + - `` — название трека (или `Track `, если `name IS NULL`). + - `` — `description`, если есть (CDATA-обёрнуто при наличии < или &). + - `` — `user`, если есть. + - `