Files
enduro-trails/docs/work-items/ET-011/08-data-requirements.md
claude-bot 6fe2ecf12b
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Successful in 6s
CI / build (push) Has been skipped
architect(ET): auto-commit from architect run_id=64
2026-06-03 20:44:55 +00:00

342 lines
18 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.
---
type: data-requirements
work_item_id: ET-011
title: "Требования к данным — ET-011: Скачивание трека из popup"
version: 1
status: approved
created_at: 2026-06-03
authors:
- "agent:architect"
---
# Требования к данным — ET-011
## 1. Резюме
ET-011 — **read-only data event**. Никаких изменений схемы БД,
никаких новых таблиц, индексов, миграций, localStorage-ключей. Эндпоинт
`GET /api/gps-tracks/{id}/download` собирает GPX-файл из существующих
полей таблицы `tracks` (ET-008 / ADR-005), переиспользует существующий
WKB-парсер (`mvt.py::_wkb_to_coords`), не пишет ни в одну таблицу.
**Меняется:**
- Содержимое `config/gps_sources.yaml` (одно optional-поле
`download_allowed: bool` per-source; см. ADR-015).
- Контракт API расширяется одним новым endpoint'ом (`/download`).
**Не меняется:**
- Schema таблиц `tracks`, `pipeline_runs`;
- Контракты существующих API `/api/gps-tracks`, `/tiles/...`, `/health`,
`/cache/clear`;
- localStorage ключи и значения клиента;
- Dedup-алгоритм (`compute_dedup_key`);
- ACTIVITY_TYPES enum;
- Маппинги `SOURCE_ATTRIBUTIONS`, `SOURCE_LABELS`.
## 2. Архитектурные границы данных
| Слой данных | Тип | Расположение | Изменения в ET-011 |
|---|---|---|---|
| OSM-vector (`trails`) | существующий | `/app/data/centralfederal.sqlite` | **нет** |
| Личные GPX треки (ET-006) | существующий | браузер (memory) | **нет** |
| Публичные GPS треки (ET-008) | существующий | `/app/data/gps_tracks.sqlite` | **read-only**: новый запрос на скачивание; никаких INSERT/UPDATE/DELETE |
| OSRM-граф | существующий | `/app/data/enduro.osrm.*` | **нет** |
| User UI state | существующий | `localStorage` | **нет** новых ключей |
| Скачанный GPX-файл | **новое (выход)** | downloads-папка браузера пользователя | формат GPX 1.1, см. §4 |
## 3. Серверные данные — `gps_tracks.sqlite`
### 3.1 Schema
**Без изменений vs ET-008.** См. `docs/work-items/ET-008/08-data-requirements.md`
§3.1, §3.5. Никаких ALTER TABLE / DROP COLUMN / CREATE INDEX.
### 3.2 Используемые поля в SELECT для /download
| Поле | Использование |
|---|---|
| `id` | Path-параметр запроса; PK lookup |
| `name` | `<metadata><name>` и `<trk><name>` в GPX; имя файла |
| `description` | `<metadata><desc>` (если не null) |
| `activity_type` | `<trk><type>` |
| `user` | `<metadata><author><name>` (если не null; для OSM по ADR-009) |
| `created_at` | `<metadata><time>` (если не null; ISO-8601 UTC) |
| `length_m` | информативно, в GPX не входит |
| `points_count` | проверка cap REQ-NF-02 (> 200000 → 413) |
| `geom` (WKB) | парсится через `_wkb_to_coords()` в `[(lon, lat), ...]`; каждая пара → один `<trkpt>` |
| `sources_json` | license-guard ADR-015; `<link>` элементы в `<metadata>` |
| `external_urls_json` | `<link href=…>` элементы; ответ 403 для CTA |
| `dedup_key`, `tags_json`, `inserted_at`, `updated_at`, `min_lon..max_lat` | не используется в /download |
### 3.3 SQL-запрос
```sql
SELECT id, name, description, activity_type, user, created_at,
length_m, points_count, geom, sources_json, external_urls_json
FROM tracks WHERE id = ?
```
Один параметр `?` — integer, валидируется FastAPI. Использует
автоматический PRIMARY KEY-индекс. Стоимость: ~1 ms.
### 3.4 Кэширование на стороне сервера
**Не вводим.** Mvt-кэш ET-008 — другой механизм (по `(z,x,y)`). Для
скачивания одиночного трека:
- Кэш-хит редкий (пользователь обычно качает один раз).
- Размер GPX до 20 МБ × N треков — раздуло бы LRU-кэш и заняло RAM.
- Производительность сборки и так в бюджете (REQ-NF-01 = 300 ms p95).
Клиентский кэш — через заголовок `Cache-Control: private, max-age=3600`
(см. ADR-014 §6). Браузер сам кэширует blob.
### 3.5 Изменения объёма БД
**Нет.** Эндпоинт read-only.
### 3.6 Backup retention
Без изменений (см. ET-008 §9).
## 4. Контракт GPX-файла (выходные данные)
### 4.1 Структура XML
```xml
<?xml version="1.0" encoding="UTF-8"?>
<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 | "Без названия"}</name>
<desc>{tracks.description}</desc> <!-- если не null -->
<author>
<name>{tracks.user}</name> <!-- если не null -->
</author>
<link href="{external_urls[0]}">
<text>Источник: {sources[0]}</text>
</link>
<!-- ... по одному <link> на каждый external_url -->
<time>{tracks.created_at | ISO-8601 UTC}</time> <!-- если не null -->
<copyright author="Enduro Trails"> <!-- если "osm" ∈ sources -->
<license>https://www.openstreetmap.org/copyright</license>
</copyright>
</metadata>
<trk>
<name>{tracks.name | "Без названия"}</name>
<type>{tracks.activity_type | "other"}</type>
<trkseg>
<trkpt lat="55.123456" lon="37.654321" />
<!-- ... по одному <trkpt> на каждую координату из geom -->
</trkseg>
</trk>
</gpx>
```
### 4.2 Соответствие схеме
Валидируется по `http://www.topografix.com/GPX/1/1/gpx.xsd` без
ошибок и warnings (REQ-NF-03, AC-5). Тестовая фикстура
`tests/fixtures/gpx-1.1/gpx.xsd` (snapshot схемы).
### 4.3 Размер и плотность
| Кол-во точек | Типичный размер | Время сборки (4-worker uvicorn) |
|---|---|---|
| 100 | ~ 15 КБ | < 5 мс |
| 1 000 | ~ 130 КБ | < 20 мс |
| 5 000 | ~ 650 КБ | < 50 мс |
| 50 000 | ~ 6.5 МБ | 80150 мс |
| 200 000 (cap) | ~ 26 МБ | 400500 мс |
| > 200 000 | — | **413 Payload Too Large** |
Округление координат `%.6f` — точность ≈ 0.11 м (более чем достаточно
для эндуро-навигации; экономит ~30% bytes vs Python-default float repr).
### 4.4 Кодировка
UTF-8 строго. `Content-Type: application/gpx+xml; charset=utf-8`.
ElementTree сам выдаёт UTF-8 при `tostring(root, encoding="utf-8",
xml_declaration=True)`.
### 4.5 Что НЕ попадает в GPX
| Поле | Причина |
|---|---|
| `<ele>` (высота) | Не хранится в БД (BRD A2 / ET-008 ограничение) |
| `<time>` в каждом `<trkpt>` | Не хранится в БД (BRD A2) |
| `<wpt>` (waypoints) | Не moнодим из треков |
| `<rte>` (роуты) | Не применимо для public GPS-tracks |
| `<extensions>` | Минимализм; кастомные расширения — отдельная фича |
| `tracks.dedup_key`, `tracks.length_m`, `tracks.points_count` | Внутренние метаданные, не часть GPX-стандарта |
| `tracks.tags_json` | В этой итерации не нужны; если потребуется — `<keywords>` в metadata |
## 5. Конфигурация — `gps_sources.yaml`
### 5.1 Новое поле `download_allowed`
| Поле | Тип | Default | Назначение |
|---|---|---|---|
| `download_allowed` | bool | `false` (если отсутствует — deny) | Управляет ответом 403 в `/download` эндпоинте |
Финальные значения для ET-011 (закрытие BRD Q-1):
| `source.id` | `download_allowed` | Юридическое основание |
|---|---|---|
| `osm` | `true` | ODbL разрешает реэкспорт с атрибуцией (ADR-009 + ADR-015 §«Решение D») |
| `enduro_russia` | `false` | Default-deny; ADR-010 ничего не говорит про реэкспорт |
| `wikiloc` | `false` | ToS Wikiloc запрещает массовый ре-экспорт (ADR-012) |
| `ttrails` | `false` | ADR-011 в `proposed`; не собирается и не отдаётся |
### 5.2 Влияние на pipeline
`gps-collector` **игнорирует** новое поле (pipeline-код не обращается к
`download_allowed`). Это redistribution-only флаг.
## 6. Контракт публичного API
### 6.1 `GET /api/gps-tracks/{track_id}/download` — **новый**
#### Параметры
| Параметр | Тип | Где | Обязательный | Default |
|---|---|---|---|---|
| `track_id` | int (ge=1) | path | да | — |
| `format` | str | query | нет | `"gpx"` (whitelist `{"gpx"}`) |
#### Ответы
| Статус | Body | Headers (ключевые) | Триггер |
|---|---|---|---|
| 200 | XML (GPX 1.1) | `Content-Type: application/gpx+xml; charset=utf-8`<br>`Content-Disposition: attachment; filename="…"; filename*=UTF-8''…`<br>`Cache-Control: private, max-age=3600`<br>`Content-Length: <bytes>` | happy path |
| 400 | `{"detail": "unsupported_format"}` | стандартные | `format` не в whitelist |
| 403 | `{"detail": "source_forbidden", "external_urls": [...]}` | стандартные | Ни один source трека не в `download_allowed` whitelist (ADR-015) |
| 404 | `{"detail": "track_not_found"}` | стандартные | Трек с указанным `id` отсутствует в БД |
| 413 | `{"detail": "track_too_large"}` | стандартные | `tracks.points_count > 200000` |
| 500 | `{"detail": "internal_error"}` | стандартные | необработанное исключение (db read fail, XML build fail) |
#### Кодирование имени файла
RFC 5987:
- `filename="<ascii_fallback>.gpx"` — ASCII-printable санитизированное
имя (см. ADR-014 §F).
- `filename*=UTF-8''<percent_encoded>.gpx` — UTF-8 имя через
`urllib.parse.quote(name, safe='', encoding='utf-8')`.
Пример (`name = "По грязи к Чёрному озеру"`):
```
Content-Disposition: attachment; filename="track-42.gpx"; filename*=UTF-8''%D0%9F%D0%BE%20%D0%B3%D1%80%D1%8F%D0%B7%D0%B8%20%D0%BA%20%D0%A7%D1%91%D1%80%D0%BD%D0%BE%D0%BC%D1%83%20%D0%BE%D0%B7%D0%B5%D1%80%D1%83.gpx
```
ASCII-fallback `track-42.gpx` используется только если у пользователя
браузер не понимает `filename*` (последние 10+ лет — не встречается).
### 6.2 Существующие эндпоинты — без изменений
`GET /api/gps-tracks`, `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt`,
`GET /api/gps-tracks/health`, `POST /api/gps-tracks/cache/clear`
без изменений.
## 7. Клиентское хранилище
### 7.1 localStorage
**Без изменений.** Никаких новых ключей. Существующие ключи ET-008
(`gps-tracks-enabled`, `gps-tracks-activities`, `gps-tracks-sources`,
`gps-tracks-color-mode`) — без изменений.
### 7.2 Не-персистентное состояние
`window.gpsTracksLayer` — без изменений.
`SOURCE_ATTRIBUTIONS`, `SOURCE_LABELS` маппинги — без изменений.
## 8. Персональные данные (PII)
| Канал | PII | Обработка в ET-011 |
|---|---|---|
| `<author><name>` в скачанном GPX | возможно (OSM user-name) | попадает только для OSM (ADR-009 collect_user_field: true). Для EnduroRussia/Wikiloc/ttrails — null в БД, элемент опускается |
| `<metadata><desc>` | возможно (свободный текст автора) | только для OSM-источника при ANY-rule ADR-015 трек качается; для не-OSM — `<copyright>` не указывается, но `<desc>` может содержать merged-text. Это **сознательный** компромисс ADR-015 §B (см. R-3 в `10-tech-risks.md`) |
| `<link href=…>` external_urls | URL-ы могут указывать на профиль автора | сохранены как есть в `external_urls_json` (паттерн ET-008) |
| IP клиента в логах скачивания | стандартный uvicorn access-log | без изменений; ротация в Docker |
### 8.1 Право на удаление
Без изменений. Удаление записи из `tracks` (ET-008 §7.1) автоматически
делает её недоступной через `/download` (404).
### 8.2 GDPR / РФ ФЗ-152
Обрабатываются только публично выложенные данные с условием
`download_allowed: true`. ODbL OSM покрывает реэкспорт (ADR-009).
## 9. Атрибуция
В скачанном GPX:
- `<copyright>` с OSM-license URL — если `"osm" ∈ sources`.
- `<link>` для каждого `external_url` — атрибуция в виде ссылок,
кликабельная в любом GPX-просмотрщике (OsmAnd, Garmin BaseCamp, QGIS).
- `creator="Enduro Trails"` в корневом `<gpx>` — атрибуция нашего
сервиса.
В UI: без изменений (MapLibre Attribution control остаётся как в ET-008).
## 10. Backup и retention
**Не применимо** к ET-011. Эндпоинт read-only, не создаёт persistent-
артефактов.
## 11. Тестовые данные (фикстуры)
### 11.1 Новые фикстуры
| Файл | Содержимое | Использование |
|---|---|---|
| `tests/fixtures/gpx-1.1/gpx.xsd` | XSD-схема topografix 1.1 (~30 КБ), скачана один раз | UT-03, IT-07 (валидация выходного GPX) |
| `tests/fixtures/gps-tracks/sample-tracks-fixture.sql` | (опц.) набор INSERT для трёх кейсов: OSM-трек 5 точек, EnduroRussia-трек 50 точек, Wikiloc-трек 100 точек | IT-01..08 |
`gpx.xsd` коммитится один раз; не зависит от внешних сервисов в
runtime (только на момент UT-теста).
### 11.2 Юридический статус фикстур
`gpx.xsd` — открытый XML Schema от `topografix.com`, свободно
распространяемый (см. footer на topografix.com). Хранение в репо для
тестирования — стандартная практика.
Тестовые SQL-фикстуры с координатами — синтетические (рандомные),
не содержат реальных треков от публикаторов.
## 12. Контракты, которые нельзя ломать
1. **Schema `tracks`, `pipeline_runs`** — не меняются (read-only
эндпоинт).
2. **Структура GeoJSON и MVT** на других эндпоинтах — не меняется.
3. **GPX 1.1 формат выходного файла** — соответствует topografix XSD;
изменение структуры (например, добавление `<extensions>`) — breaking
change для пользователей, которые уже импортировали в свои навигаторы;
требует minor-bump в `creator="Enduro Trails"` или отдельной фичи.
4. **`download_allowed` поле в `gps_sources.yaml`** — optional, default
`false`; никогда не делать его required (поломает все существующие
конфиги). Pipeline не должен начать читать это поле в будущем —
разделение confidently distinct concerns.
5. **Ответ 403 schema**`{"detail": "source_forbidden", "external_urls": [...]}`
— клиент использует `external_urls[0]` для CTA. Удаление поля
сломает UX.
## 13. Вывод
ET-011 — **read-only data event**:
- Не меняет схему БД, не добавляет миграции, не вводит новые таблицы;
- Использует существующие данные в `tracks` через один SELECT;
- Возвращает новый артефакт (GPX-файл) пользователю — не сохраняет на
сервер;
- Расширяет один конфиг-файл одним optional-полем;
- Поддерживает default-deny для лицензионной чистоты.
Юридически защищён через ADR-009 (OSM ODbL) + ADR-015 (default-deny
whitelist). Pipeline-collector не затронут.