---
type: infra-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 — **API-extension only**. Добавляется один эндпоинт в
существующий router `/api/gps-tracks/*` + правки UI-модуля
`gps_tracks.js`. Инфраструктура **не меняется**:

- 0 новых docker-сервисов;
- 0 новых файлов БД;
- 0 новых cron-записей;
- 0 новых env / секретов / API-ключей;
- 0 новых исходящих HTTPS-соединений;
- 0 новых портов и nginx-правил.

Все изменения локализованы в:
- `src/api/gps_tracks/export.py` (новый, ~130 строк)
- `src/api/gps_tracks/endpoint.py` (+1 route, ~50 строк)
- `src/api/gps_tracks/config.py` (+1 optional поле в Pydantic-модели)
- `src/api/main.py` (или эквивалент — +1 аргумент при include_router)
- `src/web/gps_tracks.js` (+обработчик + правка popup)
- `src/web/app.css` (+стиль кнопки)
- `config/gps_sources.yaml` (+per-source флаг `download_allowed`)
- tests (3 новых файла + расширение существующих)

Эскалация: **minor change** (см. ADR-014 §«Классификация», ADR-015
§«Классификация»).

## 2. Контейнеры и сервисы

| Аспект | Требование |
|---|---|
| Новый сервис | **Нет** |
| Изменения `Dockerfile` | **Нет** |
| Изменения `docker-compose.yml` | **Нет** |
| Перезапуск API после деплоя | Нужен — `docker compose up -d --no-deps app` (≈ 5 сек простоя). Подхватывает новый route + обновлённые `src/web/*.js`/`*.css`/`gps_tracks.js` |
| Перезапуск `gps-collector` | Не нужен — pipeline не затронут (collector использует тот же `gps_sources.yaml`, но игнорирует новое optional-поле `download_allowed`) |

### 2.1 Зависимости между сервисами

Без изменений. Новый эндпоинт `GET /api/gps-tracks/{id}/download`
обслуживается тем же контейнером `app`, читает ту же БД
`/app/data/gps_tracks.sqlite`.

## 3. Сеть

| Аспект | Требование |
|---|---|
| Новые входящие порты | **Нет** |
| Изменения nginx | **Нет** (новый route попадает под существующий `location /enduro/api/`) |
| Новые исходящие соединения с mva154 | **Нет** |
| CORS | Без изменений; middleware уже отдаёт `Access-Control-Allow-Origin: *` для всего `/api/` |

### 3.1 Egress trafик

Скачивание GPX — **только** в downstream браузер. Один типичный трек
≈ 800 КБ (5000 точек) или ≤ 8 МБ (50000 точек). Cap REQ-NF-02:
максимум 200000 точек ⇒ ≤ 20 МБ на запрос.

Пиковая оценка: при 20 одновременных скачиваниях типичных треков —
≈ 16 МБ/сек egress; в норме 1–2 одновременно. Не блокирует канал
test-сервера (uplink ≥ 100 Mbps по DuckDNS).

### 3.2 Rate-limit на эндпоинт

**Не вводим** в этой итерации (BRD §5 «out of scope»). Если в проде
будет аномальный трафик — добавляем `slowapi`-middleware в отдельном
DevOps-task'е (out of ET-011).

## 4. Хранилища данных

| Аспект | Требование |
|---|---|
| Новые БД | **Нет** |
| Изменения схемы `tracks` / `pipeline_runs` | **Нет** |
| Миграции | **Нет** |
| Новые SELECT-запросы | Один: `SELECT … FROM tracks WHERE id = ?` (использует PK-индекс, O(log n)) |
| Новые INSERT/UPDATE | **Нет** (эндпоинт read-only) |
| Backup | Без изменений |

### 4.1 Производительность БД

Запрос по PK — ~ 1 ms на test-сервере. Сборка GPX через
`xml.etree.ElementTree`: 5000 точек ≈ 30 ms, 50000 точек ≈ 150 ms,
200000 точек (cap) ≈ 500 ms. Бюджет REQ-NF-01 = 300 ms p95 для
50k точек — соблюдается с запасом.

`_wkb_to_coords` (переиспользуется из `mvt.py`) — уже бенчмаркнут в
ET-008: ≈ 1 ms на 1000 точек.

## 5. Конфигурация и секреты

| Аспект | Требование |
|---|---|
| Новые env-переменные | **Нет** |
| Новые секреты / API-ключи | **Нет** |
| Новые конфиг-файлы | **Нет**; меняется только содержимое `config/gps_sources.yaml` (+optional поле) |

### 5.1 Изменения `config/gps_sources.yaml`

Добавляется одно поле `download_allowed: bool` per-source. Финальные
значения для ET-011 (см. ADR-015 §«Решение D»):

```yaml
sources:
  - id: osm
    # ... existing fields unchanged
    download_allowed: true

  - id: enduro_russia
    # ... existing fields unchanged
    download_allowed: false

  - id: wikiloc
    # ... existing fields unchanged
    download_allowed: false

  - id: ttrails
    # ... existing fields unchanged
    download_allowed: false
```

Все остальные поля (`enabled`, `license_adr`, `base_url`,
`rate_limit_sec`, `user_agent`, `attribution`, `parser_module`,
`source_priority`, …) — без изменений.

### 5.2 Перечитывание конфига

`gps_sources.yaml` читается **при старте контейнера app** (один раз) —
в момент `create_gps_router(db_path, sources_config_path)`. Для
изменения политики `download_allowed` — `docker compose up -d --no-deps app`
(≈ 5 сек простоя).

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

| Аспект | Требование |
|---|---|
| Новые Python-пакеты (runtime) | **Нет** (`xml.etree.ElementTree`, `urllib.parse` — stdlib Python 3.12) |
| Новые Python-пакеты (dev) | `lxml` (для XSD-валидации в UT-03 / IT-07). Возможно уже присутствует через `defusedxml`; добавить в `requirements-dev.txt` если отсутствует. ~3 МБ |
| Новые JS-зависимости | **Нет** (vanilla JS + MapLibre API уже доступен) |
| Системные библиотеки в Dockerfile | **Нет** |
| Версия Python | 3.12, без изменений |

### 6.1 XSD-фикстура

Файл `tests/fixtures/gpx-1.1/gpx.xsd` (~30 КБ) — скачивается **один
раз** разработчиком из `http://www.topografix.com/GPX/1/1/gpx.xsd`,
коммитится в репо. Не зависит от runtime, не часть production-образа
(на `.dockerignore` уровне `tests/` уже исключён, если нет — проверить).

## 7. Сборка и деплой

### 7.1 Pipeline CI

Существующий Gitea Actions:

- `make lint` (ruff + eslint) — должен пройти без замечаний по новому
  коду (`export.py`, правки `endpoint.py`, `gps_tracks.js`).
- `make test` — должен включать новые тесты:
  - `tests/api/test_gps_tracks_gpx_builder.py` (UT-01..05)
  - `tests/api/test_gps_tracks_filename.py` (UT-04 cases)
  - `tests/api/test_gps_tracks_download.py` (IT-01..08)
  - `tests/web/test_track_download.spec.ts` (E2E-01..04)
- `make build` — пересобирает образ (никаких изменений в Dockerfile;
  но новые тестовые фикстуры и `gpx.xsd` попадают в репо).

### 7.2 Деплой шаг-за-шагом

1. `git pull origin main` на mva154.
2. `docker compose build` (опционально; никаких изменений в
   Dockerfile/requirements не было).
3. `docker compose up -d --no-deps app` — рестарт API (≈ 5 сек
   простоя) для подхвата:
   - нового эндпоинта `/api/gps-tracks/{id}/download`;
   - обновлённого `src/web/gps_tracks.js` (popup + handler);
   - обновлённого `src/web/app.css` (стили кнопки);
   - расширенного `config/gps_sources.yaml`.
4. Smoke в UI:
   - Открыть https://openclaw.mva154.duckdns.org/enduro/
   - Включить «Публичные треки», тапнуть OSM-трек → видна кнопка
     «Скачать» → клик → файл `<name>.gpx` в загрузках.
   - Тапнуть EnduroRussia-трек → клик «Скачать» → toast «Источник
     запрещает скачивание…» с ссылкой на сайт источника.
5. Smoke API:
   ```bash
   curl -I https://openclaw.mva154.duckdns.org/enduro/api/gps-tracks/<osm-track-id>/download
   # ожидаемо: HTTP 200, Content-Type: application/gpx+xml, Content-Disposition: attachment; filename*=UTF-8''…

   curl -I https://openclaw.mva154.duckdns.org/enduro/api/gps-tracks/99999999/download
   # ожидаемо: HTTP 404
   ```
6. Зафиксировать результат в `docs/work-items/ET-011/14-deploy-log.md`.

### 7.3 Время простоя

API: ≤ 5 секунд на шаге 3 (стандартный рестарт контейнера).
Pipeline: не задействован.

### 7.4 Rollback

| Сценарий | Действие | Время |
|---|---|---|
| Откат всего ET-011 | `git revert <merge-commit>` + `docker compose up -d --no-deps app` | ≈ 2 мин |
| «Выключить» новый эндпоинт без отката кода | Закомментировать `@router.get("/{track_id}/download")` или поставить `download_allowed: false` для всех источников в `gps_sources.yaml` + рестарт API | ≈ 1 мин |
| Откат БД | Не применимо (схема не менялась) | n/a |

## 8. Cron / scheduled jobs

**Нет** новых cron в ET-011. Существующий cron `gps-collector` (ET-008,
Mon+Thu 03:00 UTC) — без изменений; ET-011 не затрагивает collection.

## 9. Ресурсы (CPU / RAM / диск)

### 9.1 API-контейнер

| Метрика | Изменение | Комментарий |
|---|---|---|
| RAM idle | без изменений | загрузка `gps_sources.yaml` — < 10 КБ |
| RAM на один запрос /download | +5 МБ на 50k точек, +20 МБ на cap 200k | в пиковом сценарии 10 параллельных скачиваний по 200k = +200 МБ; в реальности 1–2 параллельно |
| CPU per запрос | 100–500 мс worker'а | ниже ETC-008 MVT-сборки |
| Disk write | 0 | эндпоинт read-only |
| Disk read | размер записи в `tracks` (geom ≈ 200 КБ для 50k точек) | через PK-индекс |

Никаких изменений `cpus:` / `mem_limit:` в `docker-compose.yml`.

### 9.2 gps-collector контейнер

Не задействован.

### 9.3 Диск

| Аспект | Изменение |
|---|---|
| `data/gps_tracks.sqlite` | без изменений (read-only эндпоинт) |
| `tests/fixtures/gpx-1.1/gpx.xsd` | +30 КБ в репо (не в production-образе) |
| Production-образ docker | без изменений (`tests/` исключены) |

## 10. Наблюдаемость

| Артефакт | Состояние после ET-011 |
|---|---|
| `uvicorn` access-log | Новые строки `200 GET /api/gps-tracks/<id>/download` (через стандартный middleware) |
| Структурный лог (stdout) | Новая строка `track_download id=<id> sources=<csv> size_bytes=<n>` на каждое 200-скачивание (через `logging.getLogger("uvicorn.access").info`) |
| 4xx/5xx | Видны в access-log в обычном формате; 5xx — stderr с traceback |
| `GET /api/gps-tracks/health` | Без изменений (download — read-only, не влияет на counters) |
| Метрики (Prometheus / OpenMetrics) | Не вводим (REQ-NF-06 явно отказывается от метрик в этой итерации) |

### 10.1 Алерты

**Нет** новых алертов. При появлении в логах систематических 500 —
ручной разбор stack-trace.

### 10.2 Logrotate

Без изменений (uvicorn пишет в stdout, Docker logger справляется).

## 11. Безопасность

| Vector | Митигация |
|---|---|
| SQL-injection через `track_id` | `track_id: int = Path(..., ge=1)` — FastAPI/Pydantic валидация, далее parameterized SQL |
| Path-traversal в имени файла на диске пользователя | `safe_filename()` заменяет `/ \ : * ? " < > |` на `_`, триммит управляющие символы; см. ADR-014 §F |
| XSS через `tracks.name` в GPX | `xml.etree.ElementTree` экранирует текст и атрибуты автоматически; integration-тест IT-07 валидирует через XSD |
| XML-bomb / external entity в **сгенерированном** GPX | N/A — мы только генерируем, не парсим. `xml.etree.ElementTree` (для сборки) не подвержен XXE |
| Утечка PII через скачанный GPX | `tracks.user` есть только для OSM (ADR-009 разрешает по ODbL); для остальных — `null` в БД (ADR-010/012); попадает в `<author>` только если присутствует |
| Утечка proprietary metadata через `<desc>` / `<name>` | Для OSM-источника — публичные данные; для не-OSM — `<copyright>` опускается (ADR-014 §G); если merged через ANY-rule (ADR-015 §B) — компромисс зафиксирован в ADR-015 |
| Утечка лицензионно-защищённой геометрии | License-guard (ADR-015) — 403 для не-разрешённых источников |
| DoS через скачивание трека 50000+ точек | Cap REQ-NF-02 ⇒ 413 для > 200000 точек; rate-limit на API — out of scope |
| Чтение чужой БД через mounted volume | Без изменений (контейнер запускается с user `appuser`, volume `/app/data` read-write только для приложения) |

### 11.1 Лицензионные атаки (юридические риски)

Покрыты ADR-015 (default-deny whitelist). Любой источник без явного
`download_allowed: true` — недоступен для скачивания. См. `10-tech-risks.md`
R-9.

## 12. Влияние на C4 / архитектурную документацию

### 12.1 Обновления `docs/architecture/README.md`

В разделе «GPS Tracks Pipeline (ET-008) → Клиентский слой публичных
треков» добавить **одну** строку после описания GeoJSON-эндпоинта:

```
- скачивание одного трека через `GET /api/gps-tracks/{track_id}/download`
  (GPX 1.1) — разрешено только для источников с
  `download_allowed: true` в `config/gps_sources.yaml` (ET-011 / ADR-014 / ADR-015).
```

### 12.2 Обновления `docs/architecture/adr/README.md`

Добавить две строки в таблице индекса ADR:

| # | Решение | Статус | Дата | Источник |
|---|---------|--------|------|----------|
| ADR-014 | GPX-download эндпоинт публичного трека: `xml.etree.ElementTree`-builder + fetch+Blob на клиенте | accepted | 2026-06-03 | [ET-011](../../work-items/ET-011/06-adr/ADR-014-gpx-download-endpoint.md) |
| ADR-015 | Политика реэкспорта публичных треков: per-source `download_allowed` в `gps_sources.yaml`, default-deny | accepted | 2026-06-03 | [ET-011](../../work-items/ET-011/06-adr/ADR-015-source-redistribution-policy.md) |

### 12.3 C4 mmd-диаграммы

В проекте отсутствуют (см. ET-008 §12, ET-009 §12). ET-011 не вводит
новых компонентов или контейнеров — обновление диаграмм не требуется.

## 13. Вывод

ET-011 — **minimal-change** на инфра-уровне:

- 0 новых сервисов / 0 новых БД / 0 миграций / 0 новых cron / 0 новых env / 0 новых портов / 0 новых runtime-зависимостей;
- Все изменения локализованы в src-коде, тестах, одной опциональной
  ячейке `gps_sources.yaml`;
- Деплой = git pull + рестарт API;
- Rollback = `git revert` или конфиг-флаг.

Эскалация: **не требуется** (`arch:major-change` не выставлен; см.
ADR-014, ADR-015).
