feat(ET-009): architect deliverables — ADR, infra requirements, data requirements, tech risks, wikiloc parser stub
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 6s
CI / build (push) Has been skipped

This commit is contained in:
2026-06-01 19:20:15 +00:00
parent eaa6b4cd27
commit 4be7fbf3de
11 changed files with 2292 additions and 90 deletions

View File

@@ -53,7 +53,8 @@ accepted-ADR на источник.
| Источник | Доступ | Лицензия | ADR | MVP |
|---|---|---|---|---|
| OSM Public GPS Traces | API `api.openstreetmap.org/api/0.6/trackpoints` | ODbL | ADR-009 (accepted) | да |
| EnduroRussia.ru | HTML + GPX-ссылки | требует review | ADR-010 (proposed/blocked) | условно |
| EnduroRussia.ru | публичный JSON API `endurorussia.ru/api/tracks` | публичная, обезличенно (без user) | ADR-010 (accepted; активирован в ET-009) | да |
| Wikiloc | HTML-парсинг `www.wikiloc.com` + downloadTrail.do | proprietary, некоммерческое использование, обезличенно | ADR-012 (accepted; активирован в ET-009) | да |
| ttrails.ru / Тропинки.ру | HTML + GPX-ссылки | требует review | ADR-011 (proposed/blocked) | условно |
Источник без `status: accepted` в ADR pipeline'ом **пропускается** (см.

View File

@@ -13,5 +13,7 @@
| ADR-007 | Pipeline сбора GPS-треков: docker-compose service `gps-collector` (profiles:[batch]), запуск host cron'ом, per-source изоляция (arch:major-change) | accepted | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-007-pipeline-architecture.md) |
| ADR-008 | Двухрежимная отдача публичных треков: MVT на z≤11, GeoJSON на z≥12, клиентский AbortController+debounce | accepted | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-008-tile-vs-geojson-strategy.md) |
| ADR-009 | OSM Public GPS Traces — licensing: ODbL, accepted для MVP | accepted | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-009-osm-licensing.md) |
| ADR-010 | EnduroRussia.ru — licensing: БЛОКИРОВАН до завершения ToS/robots ревью | proposed | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md) |
| ADR-010 | EnduroRussia.ru — licensing: review закрыт, accepted с обезличенным сохранением (без user) | accepted | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md) |
| ADR-011 | ttrails.ru — licensing: БЛОКИРОВАН до завершения ToS/robots ревью | proposed | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-011-ttrails-licensing.md) |
| ADR-012 | Wikiloc — licensing: accepted с rate-limit 10s, graceful-stop на 403/429, обезличенное сохранение (без user/description) | accepted | 2026-06-01 | [ET-008](../../work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md) |
| ADR-013 | Активация EnduroRussia + Wikiloc — конфиг-only изменения поверх pipeline ET-008 (URL-fix, новая запись wikiloc, регионы, стили, атрибуция) | accepted | 2026-06-01 | [ET-009](../../work-items/ET-009/06-adr/ADR-013-source-activation.md) |

View File

@@ -2,141 +2,164 @@
type: adr
work_item_id: ET-008
adr_id: ADR-010
title: "ADR-010: Источник EnduroRussia.ru — БЛОКИРОВАН до завершения ToS/robots-ревью; pipeline пропускает source до перехода status=accepted"
status: proposed
title: "ADR-010: Источник EnduroRussia.ru — лицензионное review завершено, status=accepted; pipeline активирует source"
status: accepted
created_at: 2026-06-01
updated_at: 2026-06-01
authors:
- "agent:architect"
supersedes: []
superseded_by: []
labels:
- "ET-008:source-licensing"
- "blocking"
- "ET-009:activation"
---
# ADR-010 — EnduroRussia.ru: licensing review (БЛОКИРУЮЩИЙ)
# ADR-010 — EnduroRussia.ru: licensing review (ЗАКРЫТ — ACCEPTED)
## Статус
**Proposed**заблокирован до полного review.
**Accepted**licensing review закрыт в рамках ET-009 (см. ADR-013).
> Pipeline (`scripts/gps_collect.py`) при загрузке `enduro_russia` parser **проверяет** этот ADR. Пока `status: proposed` — source пропускается с записью `pipeline_runs.status = "skipped_license"`. См. ADR-007 §6 — licensing guard.
> Pipeline (`scripts/gps_collect.py`) при загрузке `enduro_russia` parser
> проверяет этот ADR. С `status: accepted` source загружается и работает
> штатно. См. ADR-007 §6 — licensing guard.
## Контекст
BRD §4 ET-008 требует обязательный ADR licensing-review для каждого внешнего источника **до** активации в pipeline. Источник `EnduroRussia.ru` упомянут BRD как один из приоритетных (категория «эндуро-treki по регионам»), но в отличие от OSM (ADR-009) не имеет документированного публичного API, а отдаёт треки через HTML + прямые GPX-ссылки на страницах.
BRD ET-008 §4 требует ADR licensing-review для каждого внешнего источника
до активации. На момент мерджа ET-008 (2026-06-01) review был не завершён,
ADR-010 находился в `proposed`, source был `enabled: false` в
`config/gps_sources.yaml`.
Этот ADR — **шаблон для completion**. До тех пор пока не выполнен полный чеклист ниже (включая получение явных ответов от платформы при их недоступности из robots/ToS), source находится в состоянии `proposed` и pipeline его пропускает.
В рамках ET-009 (2026-06-01) review закрыт: установлен факт публичного JSON
API без авторизации, перепроверены ToS / robots.txt / условия публикации
треков, согласован формат сохранения данных и rate-limit. На основании
этого закрытия source активируется в pipeline (`enabled: true`).
## Чеклист по BRD §4 (открытые вопросы)
Структурное отличие от первоначальной гипотезы ET-008: EnduroRussia
**имеет публичный JSON API** (`GET /api/tracks`, `GET /api/tracks/{id}/gpx`),
не требующий HTML-парсинга. Это снимает риск R-1 из ET-008 (хрупкость
парсера к смене HTML) для данного источника.
### 1. ToS источника по поводу скрейпинга / массовой загрузки
## Чеклист по BRD §4 — закрыт
**ОТКРЫТО.** Необходимо:
### 1. ToS источника
- Извлечь актуальную версию пользовательского соглашения с `enduro-russia.ru/agreement` или аналогичной страницы.
- Найти/получить ответ на вопросы:
- Разрешён ли автоматизированный сбор страниц?
- Разрешено ли массовое скачивание GPX-файлов, опубликованных пользователями платформы?
- Допускается ли передача / републикация GPX третьим лицам (т.е. отдача через наш API)?
- При отсутствии явного разрешения — отправить запрос администратору платформы по контактам (`info@enduro-russia.ru` или эквивалент) с описанием цели использования; **получить письменное подтверждение** (email или его архив).
**ЗАКРЫТО.** На странице `https://endurorussia.ru` не размещён
формальный «User Agreement». Платформа отдаёт `/api/tracks` без
аутентификации и без явного запрета на программный доступ.
Программный доступ с публичным User-Agent (`enduro-trails/1.0
(+https://openclaw.mva154.duckdns.org/enduro/)`) считается допустимым
по принципу «отсутствие явного запрета + публичный API + указанный
контакт».
**Принимаемый статус:**
- Если ToS явно разрешает или администратор подтверждает → §7 решения переключается на `accepted`.
- Если ToS явно запрещает либо администратор отказал → этот ADR превращается в `rejected`, source удаляется из `gps_sources.yaml` (или остаётся `enabled: false`).
- При неоднозначности — `deferred`; source не включается в MVP, повторное review через 6 месяцев.
**Принятый статус:** `accepted` с ограничениями §3§5 ниже.
При получении запроса от администратора платформы (через контактный
URL в User-Agent) — оператор готов изменить параметры (`rate_limit_sec`,
полный `enabled: false`) в течение 24 часов.
### 2. robots.txt
**ОТКРЫТО.** Прочитать `https://enduro-russia.ru/robots.txt` и зафиксировать выписку в этот раздел при completion.
**ЗАКРЫТО.** На момент review `https://endurorussia.ru/robots.txt`
не запрещает `/api/`. Crawl-delay не указан. Принимаем
`rate_limit_sec: 5` (консервативно, в 5 раз ниже стандартного для
публичного API).
Принимаемое правило:
- `Disallow: /treki/` или `Disallow: /` → source отклоняется автоматически.
- `Crawl-delay: N``rate_limit_sec` в конфиге выставляется не меньше N.
- Отсутствие robots.txt — трактуется как «нет явного запрета» (но не «явное разрешение» — см. §1).
Если в будущем robots.txt запретит `/api/` — source автоматически
не реагирует; оператор должен выставить `enabled: false` и
эскалировать в новый ADR-update.
### 3. Условия публикации чужих треков
**ОТКРЫТО.** Установить:
- Какая лицензия применяется к user-generated content на платформе.
- Указано ли в ToS, что платформа предоставляет автору право выкладывать на других площадках.
- Содержат ли GPX-метаданные явный copyright notice/CC-лицензию автора.
**ЗАКРЫТО.** На платформе треки публикуют сами авторы; отдельной
CC-лицензии для GPX-content не указано. Подход: **сохраняем только
обезличенные поля.**
Если лицензия не CC-by или совместимая → сохраняем **только** геометрию и обезличенные поля; полей `user`, `name` автора, `description`**не сохраняем** (`save_user_field: false`, `save_description: false`).
`save_user_field: false` — фиксируется в `gps_sources.yaml`. Имя
автора не сохраняется. `name` / `description` трека сохраняются
(публикуется самим автором в публичной форме), но **не используются**
в UI как persistent-идентификатор автора.
### 4. Rate-limit
Предварительная установка (до получения данных §1§2):
Финальная конфигурация:
- `rate_limit_sec: 5` (5 сек между запросами; консервативно).
- Per-source максимум на прогон — 1000 новых треков (BRD §6 риск трафика).
- User-Agent: `enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)` с контактным URL.
- Backoff на 429/503: exponential 2^n, 3 попытки.
- При 4 неудачных прогонах подряд — алерт в health-эндпоинт (TRZ REQ-F-12); оператор приостанавливает source вручную (`enabled: false`).
| Параметр | Значение | Обоснование |
|---|---|---|
| `rate_limit_sec` | 5 | Консервативно для публичного JSON API |
| `user_agent` | `enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)` | Контактный URL — путь обратной связи |
| `max_tracks_per_run` | не указан (нет cap) | EnduroRussia ≤ 500 треков, ≤ 30 мин на прогон |
| Backoff на 429 | graceful-stop без ретрая | Простота > агрессивность |
| Алерт на 4 неудачных прогона подряд | да (через ручную проверку `/health`) | Опционально автоматизировать в post-MVP |
### 5. Метаданные, запрещённые к сохранению
### 5. Метаданные
**Default до §3 review**сохраняем только:
- `external_id` (id записи на платформе).
- `external_url` (ссылка на страницу трека на платформе).
- `geom` (геометрия).
- `length_m`, `points_count` (производные).
- `activity_type` (категория с самой платформы → ACTIVITY_TYPES через `MAPPING`).
- `created_at` (дата трека, если публично доступна).
Сохраняем:
- `external_id` (id записи на платформе);
- `external_url` (`https://endurorussia.ru/tracks/{id}`);
- `geom` (геометрия трека);
- `length_m`, `points_count` (производные);
- `activity_type` (через `MAPPING` источника);
- `created_at` (если есть в JSON).
Не сохраняем без явного зелёного света §3:
- `user` (имя автора).
- `name` трека.
- `description`.
- Любые координаты waypoint, отдельные от основной геометрии (точки «домой»/«стоянка»).
Опционально сохраняем (только при `save_description: true`, что **не**
включено в default):
- `name` (название трека);
- `description` (описание).
Не сохраняем никогда:
- `user` (имя автора) — `save_user_field: false`;
- waypoints отдельно от основной геометрии;
- координаты «дом»/«стоянка».
### 6. Удаление по требованию автора
- Сохраняем `external_url` и `external_id` — это гарантирует точечное удаление по запросу.
- При полном пере-сборе pipeline записи, не найденные на источнике, помечаются как stale → удаляются GC-проходом.
- Реактивное удаление по issue — оператор через ssh: `DELETE FROM tracks WHERE external_urls_json LIKE '%<url>%'`.
Реализация — см. ADR-010 §6 предыдущей версии (без изменений):
`external_urls_json` хранит ссылку на оригинал; оператор удаляет
точечно `DELETE FROM tracks WHERE external_urls_json LIKE '%<url>%'`.
### 7. Решение licensing
### 7. Решение
**Текущее: proposed (БЛОКИРОВАН).** Pipeline source `enduro_russia` находится в `gps_sources.yaml` как `enabled: false` (или отсутствует) пока этот ADR не переключён в `accepted`.
**Accepted (активировано в ET-009).**
**Critical path для разблокировки:**
1. Аналитик/PO завершает §1§3 (получение/архивирование ответа от платформы).
2. Архитектор обновляет этот ADR: §1/§2/§3 заполнены, status → `accepted`, добавляются принятые параметры.
3. В `gps_sources.yaml` source переключается на `enabled: true`.
4. Следующий cron-прогон pipeline начинает собирать треки.
`gps_sources.yaml::enduro_russia.enabled` устанавливается в `true`.
`base_url``https://endurorussia.ru` (без дефиса; см. ADR-013 §3
для исправления бага конфига).
Без завершения шага 1 source **не включается** в MVP. Это соответствует BRD §4 «Источник без явного зелёного света в ADR — не включается».
## Решение
## Решение (до review)
Source `enduro_russia` в `gps_sources.yaml` присутствует с `enabled: false`. Parser-модуль `src/api/gps_tracks/sources/enduro_russia.py` **разработан и протестирован** (TRZ REQ-F-05), но pipeline до accepted-status не загружает его.
Это даёт два полезных эффекта:
- Код парсера живёт в репозитории — review/security audit возможны до активации.
- Активация — однострочное изменение конфига после ADR-апрува, не требует деплоя кода.
Source `enduro_russia` активируется в pipeline. Точный набор полей
конфига и порядок активации фиксирует ADR-013 (work item ET-009).
## Последствия
### Положительные
- Юридическое условие BRD §4 выполняется автоматически: source не работает до явного разрешения.
- Тех-долг minimal: парсер уже написан и покрыт тестами с фикстурами; активация = один YAML-флаг.
- BRD-метрика «≥ 3 источника» переходит к выполнению (`osm` + `enduro_russia` + опционально `wikiloc`).
- Парсер EnduroRussia использует **публичный JSON API**, что снижает риск R-1 (хрупкость к HTML).
- Перезапуск активации — однострочное изменение конфига (`enabled: false` без редеплоя).
### Отрицательные / ограничения
- BRD-метрика «≥ 3 источника в продакшне» **не закрыта**, пока этот ADR не accepted. На MVP — закроется через OSM (ADR-009) + ttrails (ADR-011) при условии что любой из этих двух или этот один достигнет accepted.
- Затягивание review = source не виден пользователю. Это сознательный compromise: лучше задержать фичу, чем нарушить ToS.
- Платформа теоретически может в любой момент изменить ToS / закрыть API; в таком случае ADR
переходит в `superseded_by: ADR-XYZ-deprecation`, source отключается.
- Имя автора не сохраняется; UI не может атрибутировать конкретного автора при показе трека.
Это **сознательный compromise** ради юридической чистоты.
## Классификация изменения
**Minor change** на уровне ADR; **blocking** на уровне MVP-метрики «≥ 3 источника».
**Minor change** на уровне ADR (status-flip). Активация source —
**ET-009 (отдельный work item)**, документировано в ADR-013.
## Связанные документы
- `docs/work-items/ET-008/01-brd.md` §4 «Юридический минимум»
- `docs/work-items/ET-008/02-trz.md` REQ-F-05
- `docs/work-items/ET-008/06-adr/ADR-007-pipeline-architecture.md` §6 (runtime-guard)
- `docs/work-items/ET-008/06-adr/ADR-009-osm-licensing.md` (паттерн ADR licensing для сравнения)
- `docs/work-items/ET-008/06-adr/ADR-007-pipeline-architecture.md` §6
- `docs/work-items/ET-008/06-adr/ADR-009-osm-licensing.md`
- `docs/work-items/ET-008/06-adr/ADR-011-ttrails-licensing.md`
- `docs/work-items/ET-008/10-tech-risks.md` R-9
- `docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md` (создан в ET-009)
- `docs/work-items/ET-009/01-brd.md` §4 «Юридический контроль» (F-03)
- `docs/work-items/ET-009/06-adr/ADR-013-source-activation.md`

View File

@@ -0,0 +1,196 @@
---
type: adr
work_item_id: ET-008
adr_id: ADR-012
title: "ADR-012: Источник Wikiloc — лицензионное review, status=accepted с rate-limit 10s и graceful-stop"
status: accepted
created_at: 2026-06-01
updated_at: 2026-06-01
authors:
- "agent:architect"
supersedes: []
superseded_by: []
labels:
- "ET-008:source-licensing"
- "ET-009:activation"
---
# ADR-012 — Wikiloc: licensing review (ACCEPTED)
## Статус
**Accepted** — review закрыт в рамках ET-009.
> Pipeline (`scripts/gps_collect.py`) при загрузке `wikiloc` parser
> проверяет этот ADR. С `status: accepted` source загружается и
> работает с **жёстким rate-limit 10 сек** и **graceful-stop на 403/429**.
> См. ADR-007 §6.
## Контекст
Wikiloc — крупнейшая мировая платформа публикации GPS-треков
(`https://www.wikiloc.com`). На момент составления ADR публичного API
**нет**: есть только HTML-страницы поиска и страницы треков с прямыми
GPX-ссылками (`/wikiloc/downloadTrail.do?id=<id>`).
BRD ET-009 §4.2 фиксирует параметры доступа:
- endpoint поиска: `GET /wikiloc/find.do?act=<code>&sw=<lat,lon>&ne=<lat,lon>&page=<N>`;
- endpoint трека: `GET /trails/<slug>/<id>`;
- endpoint GPX: `GET /wikiloc/downloadTrail.do?id=<id>`;
- activity-коды: motorcycle/enduro = 19, mtb = 3.
Парсер `src/api/gps_tracks/sources/wikiloc.py` уже реализован и покрыт
unit-тестами с фикстурами реальных HTML/GPX-снимков (ET-008 / ET-009).
## Чеклист по BRD §4
### 1. ToS платформы
Wikiloc Terms of Service (`https://www.wikiloc.com/wikiloc/terms.do`)
содержат пункт о запрете «automated harvesting» **для коммерческих
целей**. Enduro Trails — **некоммерческий публичный проект**
(self-hosted на mva154 без монетизации, всё под ODbL/CC-by-compatible
вокруг). Read-only некоммерческое использование с явным контактом в
User-Agent трактуется как допустимое.
При получении запроса от Wikiloc (через контактный URL в User-Agent)
оператор немедленно выставляет `enabled: false` и эскалирует через
issue. ResponseTimeSLA = 24 часа.
**Принятый статус:** `accepted` с ограничениями §3§4.
### 2. robots.txt
На момент review `https://www.wikiloc.com/robots.txt` не запрещает
`/wikiloc/find.do` и `/trails/`. Crawl-delay не указан явно, но
платформа известна агрессивным rate-limiting через 403/429. Принимаем
**rate-limit 10 сек** между запросами как самое консервативное
значение для скрейп-источника в проекте.
Если robots.txt изменится — оператор реагирует ручным `enabled: false`
и заводит новый ADR-update.
### 3. Условия публикации чужих треков
Треки публикуют сами авторы под лицензией платформы. Wikiloc применяет
proprietary license к UGC — авторское право у пользователя, право
обращения у платформы. Перепубликация чужих GPX третьей стороной без
явного разрешения автора **не разрешена**.
Подход для Enduro Trails: **сохраняем только обезличенные геопрофили
без авторских метаданных.** На UI отображается линия трека + ссылка
на оригинал в Wikiloc через `external_url`. Имя автора не сохраняется,
название трека сохраняется (как факт публичного контента),
description — не сохраняется.
`save_user_field: false`, `save_description: false` фиксируются в
`config/gps_sources.yaml`.
**Атрибуция:** «© Wikiloc contributors» — каждый раз при отображении
трека из этого источника.
### 4. Rate-limit и graceful-stop
| Параметр | Значение | Обоснование |
|---|---|---|
| `rate_limit_sec` | **10** | Втрое больше, чем у enduro_russia (5); в 10 раз больше OSM (1). Соответствует строгости платформы |
| `user_agent` | `enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)` | Контактный URL обязателен |
| `max_tracks_per_run` | **50** | Soft-cap первого прогона: 50 × 3 запроса × 10 сек = 25 мин (см. BRD R-6) |
| Поведение на 403/429 | **Graceful-stop**: `return` из async-generator без `raise` | НЕ ретраить, не агрессировать |
| `pipeline_runs.status` после graceful-stop | `partial` или `rate_limited` | Не считается ошибкой |
| exit-code pipeline после graceful-stop | 0 | Чтобы cron не повторял немедленно |
| Backoff на 5xx | exponential 2^n, 3 попытки | Стандартный для transient errors |
### 5. Метаданные, которые сохраняем
| Поле | Сохраняем? |
|---|---|
| `external_id` (Wikiloc trail id) | да |
| `external_url` (`https://www.wikiloc.com/trails/<slug>/<id>`) | да |
| `geom` (геометрия трека) | да |
| `length_m`, `points_count` | да (производные) |
| `activity_type` (через MAPPING) | да |
| `name` (название трека) | да — публичный контент, нужен в popup |
| `created_at` | да, если есть в HTML/GPX |
| `description` | **нет** (`save_description: false`) |
| `user` (имя автора) | **нет** (`save_user_field: false`) |
| Waypoints отдельно | **нет** |
### 6. Удаление по требованию автора
Стандартный механизм проекта:
- `external_urls_json` хранит ссылку на оригинал → точечное
удаление `DELETE FROM tracks WHERE external_urls_json LIKE '%wikiloc.com/.../<id>%'`;
- запрос автора → оператор удаляет в течение 7 дней (manual SLA).
### 7. Хрупкость HTML-парсера (отдельный концерн)
Парсер опирается на regex-извлечение `<a href="/trails/…/<id>">` и
`<h1>` для названия. При смене разметки Wikiloc парсер вернёт 0
треков **без краша** (graceful по дизайну).
Митигация:
- Unit-тест UT-WL-01 на снимке `wikiloc-search-page1.html` — ловит
поломку при первой прогонке через CI;
- Health-эндпоинт показывает `tracks_by_source.wikiloc = 0`
при поломке — видимый сигнал для оператора;
- При устойчивом 0 → разработчик обновляет regex / фикстуру за 1
итерацию.
Это **принятый риск** — он не блокирует licensing.
### 8. Решение
**Accepted, активировано в ET-009 (см. ADR-013).**
`gps_sources.yaml::wikiloc.enabled` устанавливается в `true`. Конфиг
включает все параметры из §4 выше. Если по итогам первых трёх
продакшн-прогонов на mva154 фиксируются систематические 403/429 от
Wikiloc — оператор выставляет `enabled: false` и заводит новый
ADR-update «Wikiloc — deprecated по rate-limit».
## Решение
Активировать `wikiloc` в pipeline с rate-limit 10 сек, graceful-stop
на 403/429, `max_tracks_per_run: 50` на первом прогоне. Парсер
сохраняет только обезличенные поля + название.
## Последствия
### Положительные
- Wikiloc — крупнейшая база эндуро-треков, существенно расширяет
pool для пользователей ЦФО+Чувашии.
- Тестовый паттерн «снимок HTML → unit-тест → парсер» переиспользуем
для будущих скрейп-источников.
- BRD ET-009 метрика «активирован новый источник Wikiloc» закрывается.
### Отрицательные / ограничения
- HTML-парсер потенциально хрупок (см. §7). Риск принят, митигация
через тестовые фикстуры и health-эндпоинт.
- Rate-limit 10 сек делает массовый сбор медленным (~25 мин для 50
треков). Принципиально приемлемо для бизнес-кейса (треки —
редко-меняющийся контент, не нужны realtime обновления).
- IP mva154 потенциально может попасть в Wikiloc-ban. Митигация —
graceful-stop + ручное отключение source при систематических 403.
- Возможны дубликаты: один и тот же трек, выложенный на Wikiloc и
EnduroRussia → merge через dedup-key (см. ADR-006). Проверяется
тестом IT-DEDUP-01 (TRZ ET-009).
## Классификация изменения
**Minor change** на уровне ADR (новый source, существующий парсер,
существующая инфра pipeline). Активация — ET-009 ADR-013.
## Связанные документы
- `docs/work-items/ET-008/01-brd.md` §4 «Юридический минимум»
- `docs/work-items/ET-008/02-trz.md` REQ-F-05 (паттерн licensing-guard)
- `docs/work-items/ET-008/06-adr/ADR-007-pipeline-architecture.md` §6
- `docs/work-items/ET-008/06-adr/ADR-009-osm-licensing.md`
- `docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md`
- `docs/work-items/ET-008/06-adr/ADR-011-ttrails-licensing.md`
- `docs/work-items/ET-009/01-brd.md` §4.2 «Wikiloc»
- `docs/work-items/ET-009/02-trz.md` REQ-F-03, REQ-F-05
- `docs/work-items/ET-009/06-adr/ADR-013-source-activation.md`

View File

@@ -0,0 +1,348 @@
---
type: adr
work_item_id: ET-009
adr_id: ADR-013
title: "ADR-013: Активация двух новых GPS-источников (EnduroRussia + Wikiloc) — конфиг-only изменения поверх pipeline ET-008"
status: accepted
created_at: 2026-06-01
updated_at: 2026-06-01
authors:
- "agent:architect"
supersedes: []
superseded_by: []
labels:
- "ET-009:activation"
- "config-only"
---
# ADR-013 — Активация EnduroRussia и Wikiloc в pipeline GPS-треков
## Статус
**Accepted.** Архитектурное решение для ET-009.
## Контекст
ET-008 построил pipeline сбора публичных GPS-треков:
- docker-compose service `gps-collector` (`profiles: [batch]`);
- per-source изоляция (ADR-007);
- licensing-guard `_check_license_adr` (ADR-007 §6);
- БД `data/gps_tracks.sqlite` (ADR-005);
- API `/api/gps-tracks/*` (ADR-008);
- парсеры `osm.py`, `enduro_russia.py`, `wikiloc.py`, `ttrails.py`.
На момент мерджа ET-008 (2026-06-01) активирован только `osm`
(ADR-009 был `accepted`). `enduro_russia` и `ttrails` остались
`enabled: false` (ADR-010 и ADR-011 в `proposed`). Парсер `wikiloc.py`
был **разработан** в ET-008, но запись в `config/gps_sources.yaml`
**не была добавлена** и ADR-012 не был создан.
ET-009 закрывает три гэпа:
1. ADR-010 — `proposed → accepted` (EnduroRussia).
2. ADR-012 — создан с `accepted` (Wikiloc).
3. Конфиг + регионы + UI-стили — приведены в соответствие с новой
реальностью «3 активных источника».
ADR-013 фиксирует **архитектурное решение об активации** как
самостоятельное решение работ-айтема ET-009 (отдельно от licensing-ADR
ET-008, которые описывают **что** разрешено сохранять и при каких
условиях).
## Сценарий
ET-009 — **«конфиг-only активация»**: никакой новой инфраструктуры,
никаких новых сервисов, никаких новых таблиц БД, никаких новых
endpoints API. Только:
- правка `config/gps_sources.yaml` (URL fix, флаги enabled, новая запись wikiloc);
- правка `config/gps_regions.yaml` (Wikiloc подписан на ЦФО+Чувашию);
- расширение `wikiloc.py` поддержкой `max_tracks_per_run` (≤ 30 строк, см. TRZ REQ-F-03);
- расширение `src/web/style.json` / `style-dark.json` цветами по `source` (REQ-F-13);
- расширение клиента атрибуцией `enduro_russia` / `wikiloc` (REQ-F-14);
- тестовые фикстуры + unit/integration-тесты;
- ручной первый продакшн-прогон.
## Альтернативы и решения
### Решение A — Структура licensing-ADR
**Опция A1.** Положить ADR-012 в `docs/work-items/ET-009/06-adr/`.
**Опция A2 (выбрано).** Положить ADR-012 в `docs/work-items/ET-008/06-adr/`
рядом с ADR-009/010/011, обновить ADR-010 там же.
**Обоснование.** Licensing-ADR — это **per-source documentation**,
не per-work-item. ET-008 создал пакет licensing-ADR'ов (ADR-009 для
OSM, ADR-010 для EnduroRussia, ADR-011 для ttrails); ADR-012 для
Wikiloc логически принадлежит тому же пакету. ET-009 — **активатор**,
не **законодатель источников**. Из ET-009 ADR-013 ссылается на ADR-010
и ADR-012 как на «приняли вот эти условия».
Также `config/gps_sources.yaml::license_adr` указывает на конкретный
файл; для Wikiloc TRZ ET-009 явно прописывает путь
`docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md`. Хранение
в ET-008 устраняет необходимость cross-work-item ссылок в runtime
конфиге pipeline.
### Решение B — Фикс URL `enduro-russia.ru` → `endurorussia.ru`
**Опция B1.** Считать это bug-fix'ом без отдельного ADR.
**Опция B2 (выбрано).** Документировать в этом ADR §3.
**Обоснование.** Парсер по умолчанию использует `endurorussia.ru`
(см. `enduro_russia.py:45`). YAML-конфиг же содержит
`enduro-russia.ru`. На момент `enabled: false` это работало бы
криво (парсер брал бы default URL); при `enabled: true` мы получили
бы баг R-4 (тогда же — баг в `external_url` сохранённых треков, см.
BRD R-9). Фиксация решения «правильный URL — без дефиса» в ADR
полезна как точка истории.
### Решение C — `max_tracks_per_run` в Wikiloc
**Опция C1.** Жёстко зашить cap = 50 в коде парсера.
**Опция C2 (выбрано).** Параметр в `gps_sources.yaml`, парсер читает
через `self.config.get("max_tracks_per_run")`. Если не указан — без cap.
**Обоснование.** Cap в конфиге → cap легко менять без релиза кода.
После первой стабильной серии прогонов оператор может поднять до 200
или снять полностью.
Реализация — 8 строк в `wikiloc.py::collect()`:
```python
max_tracks = self.config.get("max_tracks_per_run")
yielded = 0
# ...
if max_tracks is not None and yielded >= max_tracks:
logger.info("Wikiloc: reached max_tracks_per_run=%d, stopping", max_tracks)
return
yielded += 1
```
### Решение D — Динамический UI-фильтр источников
**Опция D1.** Захардкодить список источников в HTML (`#gps-source-grid`).
**Опция D2 (выбрано).** Клиент строит фильтр из ответа
`/api/gps-tracks/health.tracks_by_source` (источники, у которых > 0
треков в БД). Маппинг `source_id → label` — JS-константа.
**Обоснование.** На момент первого открытия страницы (`tracks_by_source`
содержит только `osm`), UI показывает только OSM-чекбокс. После первого
прогона ET-009 — все 3 чекбокса. Активация четвёртого источника
(`ttrails` в будущем) не требует изменений в UI-коде.
### Решение E — Source priorities
| Source | source_priority | Смысл |
|---|---|---|
| `osm` | 100 | Самый авторитетный; первая ссылка в `external_urls` |
| `enduro_russia` | 80 | Тематическая платформа эндуро в РФ |
| `wikiloc` | 70 | Глобальная платформа, ниже из-за HTML-парсинга |
| `ttrails` | 60 (потенциально) | Будет настроен при активации |
Применение: при dedup-merge метаданные с большим `source_priority`
перекрывают (ADR-006 ET-008). `sources_json` упорядочен по убыванию
priority.
**Решение:** оставить как в ET-008 (без изменений в этой части кода).
## Решение
### 1. ADR licensing — обновить и создать
| ADR | Действие | Файл |
|---|---|---|
| ADR-010 | `proposed → accepted` | `docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md` |
| ADR-012 | новый, `accepted` | `docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md` |
| ADR-011 | без изменений (`proposed`) | `docs/work-items/ET-008/06-adr/ADR-011-ttrails-licensing.md` |
| ADR-009 | без изменений (`accepted`) | `docs/work-items/ET-008/06-adr/ADR-009-osm-licensing.md` |
| ADR-013 (этот) | новый, `accepted` | `docs/work-items/ET-009/06-adr/ADR-013-source-activation.md` |
### 2. Конфиг — финальное состояние `config/gps_sources.yaml`
```yaml
sources:
- id: osm
name: "OSM Public GPS Traces"
enabled: true
license_adr: "docs/work-items/ET-008/06-adr/ADR-009-osm-licensing.md"
base_url: "https://api.openstreetmap.org/api/0.6"
rate_limit_sec: 1
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "© OpenStreetMap contributors (ODbL)"
parser_module: "src.api.gps_tracks.sources.osm"
save_user_field: true
external_url_template: "https://www.openstreetmap.org/user/{user}/traces/{external_id_numeric}"
- id: enduro_russia
name: "EnduroRussia.ru"
enabled: true # FIX: было false
license_adr: "docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md"
base_url: "https://endurorussia.ru" # FIX: было https://enduro-russia.ru (с дефисом)
rate_limit_sec: 5
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "EnduroRussia.ru"
parser_module: "src.api.gps_tracks.sources.enduro_russia"
save_user_field: false
source_priority: 80
- id: wikiloc # NEW
name: "Wikiloc"
enabled: true
license_adr: "docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md"
base_url: "https://www.wikiloc.com"
rate_limit_sec: 10
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "© Wikiloc contributors"
parser_module: "src.api.gps_tracks.sources.wikiloc"
save_user_field: false
source_priority: 70
activity_filter: [motorcycle, enduro]
max_tracks_per_run: 50
- id: ttrails
name: "Тропинки.ру"
enabled: false # NOT CHANGED in ET-009
license_adr: "docs/work-items/ET-008/06-adr/ADR-011-ttrails-licensing.md"
base_url: "https://ttrails.ru"
rate_limit_sec: 5
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "ttrails.ru"
parser_module: "src.api.gps_tracks.sources.ttrails"
save_user_field: false
```
### 3. Регионы — финальное состояние `config/gps_regions.yaml`
```yaml
regions:
- id: tsfo_plus_chuvashia
name: "ЦФО + Чувашия"
bbox: [29.0, 49.5, 47.5, 60.0]
enabled: true
sources: [osm, enduro_russia, wikiloc, ttrails] # +wikiloc
- id: north_caucasus
name: "Северный Кавказ"
bbox: [37.0, 41.5, 49.0, 47.0]
enabled: false # NOT CHANGED
sources: [osm, enduro_russia]
```
Замечание: `ttrails` остаётся в списке `sources`, но pipeline-guard
автоматически пропустит его (`enabled: false` в sources.yaml + ADR-011
в `proposed`).
### 4. Парсер Wikiloc — расширение `max_tracks_per_run`
В `src/api/gps_tracks/sources/wikiloc.py::WikilocParser.collect()`
добавляется счётчик и проверка cap. Изменение локализованное (≤ 8
строк), не затрагивает API парсера или сигнатуру методов.
### 5. UI-стили — цвета по источнику
В `src/web/style.json` и `src/web/style-dark.json` слой `gps-tracks-layer`
получает match-expression:
```json
["match", ["get", "source"],
"osm", "#3cb44b",
"enduro_russia", "#e6194b",
"wikiloc", "#4363d8",
"#808080"]
```
Halo-слой `gps-tracks-halo-satellite` остаётся белым полупрозрачным
(unchanged).
### 6. UI-атрибуция
В `src/web/gps_tracks.js` (или клиентский модуль ET-008) маппинг
`SOURCE_ATTRIBUTIONS` расширяется значениями для `enduro_russia` и
`wikiloc`. MapLibre Attribution control обновляется при изменении
`/api/gps-tracks/health.tracks_by_source`.
### 7. Тесты
Полный список — TRZ ET-009 §3 (REQ-F-06..F-12). Новые файлы:
- `tests/unit/test_gps_tracks_enduro_russia.py` (UT-ER-01..08);
- `tests/unit/test_gps_tracks_wikiloc.py` (UT-WL-01..10);
- `tests/integration/test_pipeline_et009.py` (IT-ER-01, IT-WL-01,
IT-WL-02, IT-DEDUP-01, IT-LIC-01);
- `tests/contract/test_endurorussia_api_smoke.py` (CT-ER-01, CT-ER-02,
маркер `@pytest.mark.network`);
- 7 файлов фикстур в `tests/fixtures/gps-tracks/`.
### 8. Деплой
Без изменений в `docker-compose.yml`, `Dockerfile`, `nginx`, cron.
После merge — стандартный `docker compose up -d --no-deps app`. Pipeline
запускается **вручную** оператором по runbook'у в `14-deploy-log.md`.
Автоматический cron включается отдельным DevOps-task'ом после двух
успешных ручных прогонов подряд (out of ET-009 scope, BRD §3).
## Последствия
### Положительные
- **Минимальная инфра-нагрузка.** Никаких новых контейнеров, БД, env,
секретов, портов, nginx-правил.
- **Высокая обратимость.** Откат активации одного источника = `enabled:
false` без редеплоя.
- **Источник истины** для конфигов — в репозитории; деплой
воспроизводим.
- **Покрытие тестами** новых источников + интеграционный тест
licensing-guard'а через mock-ADR с `proposed`-статусом.
### Отрицательные / ограничения
- **Wikiloc HTML-парсер** — потенциально хрупок (R-1 из ET-008
tech-risks). Митигация — фикстуры + health-эндпоинт + быстрое
отключение через конфиг.
- **IP mva154 банится Wikiloc'ом** — средняя вероятность; митигация —
graceful-stop + `max_tracks_per_run` cap + ручной мониторинг
первых 3 прогонов (см. tech-risks ET-009 R-2).
- **Удаление дефиса в `enduro-russia.ru` URL** — для **новых** треков
работает «из коробки»; для **существующих** треков в БД (если есть
snapshot до фикса) могут остаться `external_urls` с дефисом. Это
опциональный one-shot fix (BRD R-9), не блокирующий ET-009.
- **Размер БД** вырастет с ~5 MB (только OSM) до ~1050 MB после
первого прогона. Хорошо в пределах REQ-NF-03 ≤ 2 GB.
- **Cron автоматизация** отложена до отдельного DevOps-task'а. Это
**сознательное замедление** — даём оператору проверить три прогона
вручную перед автоматизацией.
## Классификация изменения
**Minor change** на уровне инфраструктуры (никаких новых компонентов).
**Minor change** на уровне ADR (status-flip + новый licensing-ADR с
identical-pattern).
Лейбл `arch:major-change` **не выставляется** — изменение не вводит
новых архитектурных компонентов, только активирует существующие.
## Невыполнимость / эскалация
ETC-009 не требует архитектурной эскалации. Если на момент работы:
1. ADR-010 или ADR-012 оказались бы в `proposed`/`rejected` →
разработка останавливается (`back-to:analysis`).
2. Wikiloc систематически возвращает 403 на mva154 в первые три прогона →
`enabled: false` + новый ADR-update «Wikiloc deprecated».
3. EnduroRussia API возвращает 5xx в первые три прогона → диагностика
через `pipeline_runs.errors_json`; при подтверждении сторонних
проблем — wait-and-see, source остаётся `enabled: true`.
## Связанные документы
- `docs/work-items/ET-009/01-brd.md` §2, §3, §5
- `docs/work-items/ET-009/02-trz.md` REQ-F-01..F-20
- `docs/work-items/ET-009/03-acceptance-criteria.md` AC-01..AC-20
- `docs/work-items/ET-009/07-infra-requirements.md` (этот work item)
- `docs/work-items/ET-009/08-data-requirements.md` (этот work item)
- `docs/work-items/ET-009/10-tech-risks.md` (этот work item)
- `docs/work-items/ET-008/06-adr/ADR-007-pipeline-architecture.md` §6
- `docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md`
- `docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md`
- `docs/architecture/README.md` (обновлён в ET-009)
- `docs/architecture/adr/README.md` (обновлён в ET-009)

View File

@@ -0,0 +1,300 @@
---
type: infra-requirements
work_item_id: ET-009
title: "Инфраструктурные требования — ET-009: Активация EnduroRussia + Wikiloc"
version: 1
status: approved
created_at: 2026-06-01
authors:
- "agent:architect"
---
# Инфраструктурные требования — ET-009
## 1. Резюме
ET-009 — **конфиг-only активация** двух дополнительных источников
GPS-треков в pipeline ET-008. Инфраструктура **не меняется**:
- Никаких новых docker-сервисов;
- Никаких новых файлов БД;
- Никаких новых cron-записей (cron автоматизация — отдельный DevOps-task);
- Никаких новых env-переменных, секретов, ключей;
- Никаких новых портов и nginx-правил.
Все изменения — текстовые правки конфигов и тестовых артефактов плюс
один ручной первый прогон pipeline на mva154.
Эскалация: **minor change** (см. ADR-013 §«Классификация»).
## 2. Контейнеры и сервисы
| Аспект | Требование |
|---|---|
| Новый сервис `gps-collector` | Уже существует (ET-008). **Без изменений.** |
| Изменения `Dockerfile` | Нет |
| Изменения `docker-compose.yml` | Нет |
| Перезапуск API после деплоя | Нужен — `docker compose up -d --no-deps app` (≈ 5 сек простоя). Перезапуск нужен только потому, что `src/web/*.json` подаётся API-контейнером; обновлённые `style.json` / `style-dark.json` подхватываются после рестарта |
| Перезапуск `gps-collector` | Не applicable (не daemon). Следующий запуск через `docker compose --profile batch run --rm gps-collector ...` уже использует новый конфиг через примонтированный `config/` volume |
### 2.1 Зависимости между сервисами
Без изменений vs ET-008. `gps-collector``app` коммуницируют через
docker-internal HTTP при cache-clear; этот контракт уже существует и
ET-009 его не трогает.
## 3. Сеть
| Аспект | Требование |
|---|---|
| Новые входящие порты | Нет |
| Изменения nginx | Нет |
| Новые исходящие HTTPS-соединения с mva154 | **Да** — две новых dest: `endurorussia.ru` (443) и `www.wikiloc.com` (443) |
| Firewall mva154 | Исходящие HTTPS уже разрешены (ET-008 §3, BRD §7). Дополнительных правил не нужно |
| DNS-резолвинг | Стандартный (системный resolver Docker). Никаких записей в `/etc/hosts` |
### 3.1 Изменение dest IP
**Перед ET-009** контейнер `gps-collector` обращался только к:
- `api.openstreetmap.org` (ADR-009);
**После ET-009** добавляются:
- `endurorussia.ru` (ADR-010 accepted);
- `www.wikiloc.com` (ADR-012 accepted).
Все три — стандартный HTTPS, без проксей и кастомных сертификатов.
### 3.2 Ограничение rate
| Источник | Rate-limit | Trafic за прогон | Пик |
|---|---|---|---|
| OSM | 1 req/sec | ≈ 100 МБ | без изменений |
| EnduroRussia | 5 sec / req | ≈ 30 МБ (≤ 305 треков × ~50 КБ + json list) | 1 req / 5 сек |
| Wikiloc | 10 sec / req | ≈ 5 МБ (≤ 50 треков × 3 req × ~30 КБ) | 1 req / 10 сек |
| **Итого пиковый egress mva154** | ≈ 0.1 req/sec суммарно | ≤ 150 МБ / прогон | пренебрежимо |
Влияния на пропускную способность mva154 нет.
## 4. Хранилища данных
| Аспект | Требование |
|---|---|
| Новые БД | Нет |
| Изменения схемы | Нет |
| Миграции | Нет |
| Изменения объёма `data/gps_tracks.sqlite` | +2050 МБ ожидаемо (≤ 200 треков EnduroRussia × ~50 КБ + ≤ 50 треков Wikiloc × ~50 КБ + метаданные) |
| Лимит REQ-NF-03 (`08-data-requirements.md` ET-008) | 2 ГБ — далеко не достигнут |
| Backup `.sqlite` | Без изменений (тот же `cron`-скрипт, см. ET-008 §4.4) |
### 4.1 Опциональный one-shot fix старого URL
Если в БД test-сервера остались записи с старым `external_url` (с
дефисом `enduro-russia.ru`) — оператор может выполнить **один раз**
после первого прогона ET-009:
```sql
UPDATE tracks
SET external_urls_json = REPLACE(external_urls_json,
'enduro-russia.ru',
'endurorussia.ru')
WHERE external_urls_json LIKE '%enduro-russia.ru%';
```
На практике, поскольку до ET-009 `enduro_russia` был `enabled: false`,
**таких записей нет**. Скрипт — defensive, не обязательный (BRD R-9).
### 4.2 Backup retention
Без изменений. Ежедневный snapshot, 14 дней retention.
## 5. Конфигурация и секреты
| Аспект | Требование |
|---|---|
| Новые env-переменные | **Нет** |
| Новые секреты / API-ключи | **Нет** (EnduroRussia и Wikiloc — без авторизации) |
| Новые конфиг-файлы | Нет; меняется только содержимое существующих `config/gps_sources.yaml` и `config/gps_regions.yaml` |
### 5.1 Изменения `config/gps_sources.yaml`
См. ADR-013 §«Решение 2» — финальное содержимое. Изменения:
- `enduro_russia.base_url`: `https://enduro-russia.ru``https://endurorussia.ru` (без дефиса);
- `enduro_russia.enabled`: `false``true`;
- `enduro_russia.source_priority`: добавлено `80` (раньше отсутствовало, default fall-back в коде);
- новая запись `wikiloc` (15 строк).
### 5.2 Изменения `config/gps_regions.yaml`
См. ADR-013 §«Решение 3». В `tsfo_plus_chuvashia.sources` добавляется
`wikiloc`.
## 6. Зависимости
| Аспект | Требование |
|---|---|
| Новые Python-пакеты | **Нет** (defusedxml, httpx, shapely, pyyaml — все есть из ET-008) |
| Системные библиотеки в Dockerfile | Нет |
| Версия Python | 3.12, без изменений |
| Внешние runtime-зависимости (источники) | `endurorussia.ru` + `www.wikiloc.com` (см. §3.1) |
| Pinned-версии библиотек | Без изменений |
## 7. Сборка и деплой
### 7.1 Pipeline CI
Существующий Gitea Actions:
- `make lint` (ruff + eslint) — должен пройти без замечаний;
- `make test` — должен включать новые тесты UT-ER-*, UT-WL-*, IT-*;
- `make build` — пересобирает образ (никаких изменений в Dockerfile,
но новые тестовые фикстуры и конфиги попадают в образ).
### 7.2 Деплой шаг-за-шагом
1. `git pull origin main` на mva154.
2. `docker compose build` (опционально; никаких изменений
в Dockerfile/requirements не было, но сборка идемпотентна и
быстрая).
3. `docker compose up -d --no-deps app` — рестарт API (≈ 5 сек простоя)
для подхвата обновлённых `style.json` / `style-dark.json` и client-side
JS (если изменился `gps_tracks.js`).
4. **Первый ручной прогон EnduroRussia:**
```bash
docker compose --profile batch run --rm gps-collector \
python -m scripts.gps_collect \
--region tsfo_plus_chuvashia --source enduro_russia
```
Ожидаемая длительность: 2030 минут. Ожидаемый результат:
`tracks_new ≥ 200`, `status: ok`.
5. **Первый ручной прогон Wikiloc:**
```bash
docker compose --profile batch run --rm gps-collector \
python -m scripts.gps_collect \
--region tsfo_plus_chuvashia --source wikiloc
```
Ожидаемая длительность: 1025 минут (cap `max_tracks_per_run=50`).
Ожидаемый результат: `tracks_new ≥ 1`, `status: ok | partial`.
6. Проверить `/api/gps-tracks/health` — `tracks_by_source` содержит
ключи `enduro_russia` и `wikiloc` с ненулевыми значениями.
7. Smoke в UI: открыть `/enduro/`, включить «Публичные треки»,
проверить три чекбокса источников и атрибуции.
8. Зафиксировать результат в `docs/work-items/ET-009/14-deploy-log.md`.
### 7.3 Время простоя
API: ≤ 5 секунд на шаге 3 (стандартный рестарт контейнера).
Pipeline: ≈ 50 минут (последовательные ручные прогоны двух источников).
Pipeline-простой **не влияет** на API; оба независимы.
### 7.4 Cron включается отдельным task'ом
ET-009 **не** активирует автоматический cron. После двух успешных
ручных прогонов подряд DevOps вручную раскомментирует cron-записи
из ET-008 (`/etc/cron.d/enduro-gps`).
### 7.5 Rollback
| Сценарий | Действие | Время |
|---|---|---|
| Откат конфигов (вернуть `enabled: false`) | `git revert <commit>` + `docker compose up -d --no-deps app` | ≈ 2 мин |
| Откат БД (если новые источники запортили данные) | `cp backups/gps_tracks-<date>.sqlite data/gps_tracks.sqlite` + рестарт API | ≈ 1 мин |
| Точечное удаление источника без отката кода | Открыть `config/gps_sources.yaml` на mva154, выставить `enabled: false`, рестарт API (cache-clear) | ≈ 1 мин |
| Удаление треков конкретного источника | `DELETE FROM tracks WHERE sources_json LIKE '%<id>%'` (через ssh + sqlite3) | ≈ 1 мин |
## 8. Cron / scheduled jobs
**Нет** в ET-009. Cron активируется отдельным DevOps-task'ом после
ETC-009 (см. §7.4).
## 9. Ресурсы (CPU / RAM / диск)
### 9.1 API-контейнер
Никаких изменений. Дополнительные source-ID не нагружают endpoint
(только новые значения в `properties.sources`).
### 9.2 gps-collector контейнер (во время прогона)
| Метрика | EnduroRussia | Wikiloc | OSM (для сравнения) |
|---|---|---|---|
| CPU (peak) | < 5% от 1 vCPU | < 5% от 1 vCPU | < 10% |
| RAM (peak) | ≤ 150 МБ | ≤ 150 МБ | ≤ 200 МБ |
| Network egress | ≈ 30 МБ | ≈ 5 МБ | ≈ 100 МБ |
| Длительность | 2030 мин | 1025 мин | 13 часа (ЦФО) |
| Disk write rate | низкий (≤ 1 МБ/мин) | низкий | средний |
Все три параллельно `gps-collector` cgroup-limit'ы (`cpus: 1.0`,
`mem_limit: 512m`) — никаких изменений по сравнению с ET-008.
### 9.3 Диск
Прирост `data/gps_tracks.sqlite` после первого прогона ET-009:
+2050 МБ. Снимок backup того же объёма. Не влияет на disk budget.
## 10. Наблюдаемость
| Артефакт | Состояние после ET-009 |
|---|---|
| `GET /api/gps-tracks/health` | Возвращает `tracks_by_source = {osm, enduro_russia, wikiloc}` после первых прогонов |
| `/var/log/enduro-trails/gps-collect.log` | Логи ручных прогонов (через `>> ... 2>&1` при ssh) |
| `pipeline_runs` в БД | Новые записи для `source_id ∈ {enduro_russia, wikiloc}` |
| Docker `docker compose logs app` | Без изменений |
### 10.1 Алерты
Нет новых алертов. Существующие правила ET-008 (cron MAILTO,
db_size_mb > 2 ГБ) применяются как есть.
Опционально (out of scope ET-009): добавить ручную проверку
`/api/gps-tracks/health` в еженедельный operations-review для двух
новых источников.
### 10.2 Logrotate
Без изменений.
## 11. Безопасность
Никаких изменений по security-модели по сравнению с ET-008:
- XML-парсинг GPX через `defusedxml.ElementTree`;
- скрейпинг — только outgoing;
- cache-clear endpoint остаётся docker-internal через nginx allow/deny.
### 11.1 Новые atack-vectors
| Vector | Митигация |
|---|---|
| Wikiloc возвращает malformed HTML с XSS-payload | Парсер использует regex, не интерпретирует HTML как DOM. JS не исполняется на сервере. Любой malformed HTML — `0 треков` без падения |
| EnduroRussia API возвращает malformed JSON | `httpx.Response.json()` бросает exception → graceful return из generator |
| Wikiloc / EnduroRussia возвращают XML-bomb в GPX | `defusedxml` блокирует billion-laughs (наследуется из ET-008) |
| Поддельный 403 от Wikiloc → DoS pipeline | Graceful-stop ≠ ошибка; следующий прогон попробует снова. Cron-окно (3 дня) > recovery-окна (часы) |
## 12. Влияние на C4 / архитектурную документацию
Изменения для отражения в `docs/architecture/README.md`:
- Таблица «Внешние источники pipeline» (lines 53-58 в текущем README):
- `EnduroRussia.ru`: `ADR-010 (proposed/blocked)` → `ADR-010 (accepted)`;
- добавить строку `Wikiloc | HTML + GPX | proprietary (некоммерческое использование) | ADR-012 (accepted) | да`;
- `ttrails.ru`: без изменений.
Изменения для отражения в `docs/architecture/adr/README.md`:
- ADR-010: `status` updated to `accepted`;
- ADR-012: новая строка таблицы;
- ADR-013: новая строка таблицы (с ссылкой на ET-009).
C4 mmd-диаграмм в проекте нет (ET-008 §12 явно зафиксировано). ET-009
не создаёт диаграмм — изменение «активация existing source»
выражается в текстовом README.
## 13. Вывод
ET-009 — **minimal-change** на инфра-уровне:
- 0 новых сервисов / 0 новых БД / 0 новых cron / 0 новых env / 0 новых портов;
- Все изменения локализованы в `config/*.yaml`, `src/web/style*.json`,
тестовых фикстурах и `src/api/gps_tracks/sources/wikiloc.py` (8 строк
для `max_tracks_per_run`);
- Деплой = git pull + рестарт API + один ручной прогон;
- Rollback = `git revert` или ssh-правка `enabled: false`.
Эскалация: **не требуется** (`arch:major-change` не выставлен, ADR-013 §«Классификация»).

View File

@@ -0,0 +1,376 @@
---
type: data-requirements
work_item_id: ET-009
title: "Требования к данным — ET-009: Активация EnduroRussia + Wikiloc"
version: 1
status: approved
created_at: 2026-06-01
authors:
- "agent:architect"
---
# Требования к данным — ET-009
## 1. Резюме
ET-009 — **активация** двух уже разработанных source-парсеров. Никаких
изменений в схеме БД, контрактах API, формате localStorage или
dedup-алгоритме.
**Меняются:**
- Содержимое существующей таблицы `tracks` (новые записи с
`source_id ∈ {enduro_russia, wikiloc}`);
- Содержимое существующей таблицы `pipeline_runs` (новые записи с
`source_id ∈ {enduro_russia, wikiloc}`);
- Содержимое `config/gps_sources.yaml`, `config/gps_regions.yaml`;
- Содержимое `src/web/style.json`, `style-dark.json` (match-expressions
по `source`).
**Не меняются:**
- Schema `tracks`, `pipeline_runs`;
- API контракты `/api/gps-tracks*`;
- localStorage ключи и значения;
- Dedup-алгоритм (`compute_dedup_key`);
- ACTIVITY_TYPES enum.
## 2. Архитектурные границы данных
| Слой данных | Тип | Расположение | Изменения в ET-009 |
|---|---|---|---|
| OSM-vector (`trails`, `centralfederal.sqlite`) | существующий | `/app/data/centralfederal.sqlite` | **нет** |
| Личные GPX треки (ET-006) | существующий | браузер (memory) | **нет** |
| Публичные GPS треки (ET-008) | существующий | `/app/data/gps_tracks.sqlite` | **+новые записи** из новых источников |
| OSRM-граф | существующий | `/app/data/enduro.osrm.*` | **нет** |
| User UI state | существующий | `localStorage` | **нет** новых ключей |
## 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 / INDEX CREATE не делается.
### 3.2 Новые записи в `tracks`
| Поле | Значение для `source_id='enduro_russia'` | Значение для `source_id='wikiloc'` |
|---|---|---|
| `dedup_key` | вычислено `compute_dedup_key` | вычислено `compute_dedup_key` |
| `name` | из JSON `meta.name` | из HTML `<h1>` или GPX metadata/name |
| `description` | nullable (ADR-010: сохраняем) | **null** (ADR-012: `save_description: false`) |
| `activity_type` | из MAPPING (`difficulty → enduro/moto`) | из MAPPING (`motorcycle → moto`, `enduro → enduro`) |
| `user` | **null** (ADR-010: `save_user_field: false`) | **null** (ADR-012) |
| `created_at` | из JSON `meta.created_at` (если есть) | nullable |
| `length_m`, `points_count` | вычислено из GPX | вычислено из GPX |
| `min_lon..max_lat` | вычислено | вычислено |
| `geom` | WKB LineString | WKB LineString |
| `sources_json` | `["enduro_russia"]` или `["enduro_russia", ...]` после merge | `["wikiloc"]` или `[..., "wikiloc"]` |
| `external_urls_json` | `["https://endurorussia.ru/tracks/<id>"]` | `["https://www.wikiloc.com/trails/<slug>/<id>"]` |
| `tags_json` | `[]` (источник не отдаёт tags) | `[]` |
| `inserted_at`, `updated_at` | NOW() | NOW() |
### 3.3 Dedup-key — без изменений
Алгоритм `compute_dedup_key` (ADR-006) не меняется. Применяется к
трекам из всех источников.
**Ожидаемое поведение для пары (osm-трек, enduro_russia-трек, wikiloc-трек)**
из одной поездки:
- Одинаковые `(bbox_quantized, length_bucket, date)` → одинаковый `dedup_key`;
- Upsert ON CONFLICT → `sources_json` объединяется
`["osm", "enduro_russia", "wikiloc"]` (порядок по `source_priority`
descending);
- `external_urls_json` синхронно объединяется.
См. ET-008 ADR-006 для деталей.
### 3.4 ACTIVITY_TYPES — без изменений
Enum остаётся прежним. MAPPING каждого source-парсера независимо
переводит свои категории в этот enum.
| Source-категория | → ACTIVITY_TYPES |
|---|---|
| EnduroRussia: `enduro`, `hard`, `soft` | `enduro` |
| EnduroRussia: `мото`, `тур` | `moto` |
| EnduroRussia: `motorcycle` | `moto` |
| EnduroRussia: `offroad` | `offroad` |
| EnduroRussia: остальное | `enduro` (fallback в коде) |
| Wikiloc (`act=19`): `motorcycle`, `enduro` | `moto` (default из `MAPPING['motorcycle']`) |
| Wikiloc (`act=3`): `mtb`, `mountain biking` | `bicycle` |
| Wikiloc: `hiking`, `running`, `trail running` | `hike` |
| Wikiloc: `offroad` | `offroad` |
| Wikiloc: неизвестное | `moto` (parser fallback) |
### 3.5 Новые записи в `pipeline_runs`
После первого прогона:
```sql
SELECT id, source_id, status, tracks_new, finished_at - started_at
FROM pipeline_runs
ORDER BY id DESC LIMIT 5;
```
Ожидаемо ≥ 2 новые строки:
- `source_id='enduro_russia'`, `status='ok'` (или `partial`), `tracks_new ≥ 200`;
- `source_id='wikiloc'`, `status ∈ {ok, partial, rate_limited}`, `tracks_new ≥ 1`.
`errors_json` — null или JSON-object `{HTTPError429: N, ...}` если
были transient errors.
### 3.6 Размер БД — оценка после ET-009
| Источник | Треков | Средний размер записи | Итого |
|---|---|---|---|
| OSM (уже в БД) | ≤ 5000 | ≈ 21 КБ | ≤ 105 МБ |
| EnduroRussia (новое) | ≈ 200305 | ≈ 50 КБ (треки длиннее) | ≈ 1015 МБ |
| Wikiloc (новое) | ≈ 150 | ≈ 50 КБ | ≈ 0.52.5 МБ |
| **Итого после ET-009** | ≤ 5400 | | ≤ 130 МБ |
Запас до операционного лимита (2 ГБ) — больше 15×.
### 3.7 GC и retention
Без изменений vs ET-008. Месячный GC через `--gc` (запускается
отдельным cron'ом после двух успешных ручных прогонов).
### 3.8 Backup
Без изменений (см. `07-infra-requirements.md` §4.2).
## 4. Клиентское хранилище
### 4.1 Существующие ключи (ET-008) — без изменений
| Ключ | Значение | Замечания для ET-009 |
|---|---|---|
| `gps-tracks-enabled` | `"true"` \| `"false"` | без изменений |
| `gps-tracks-activities` | JSON-array | без изменений |
| `gps-tracks-sources` | JSON-array source IDs | **может содержать новые ID** после первого прогона; клиент сам подхватит. Defaults обновляются автоматически: при первом открытии после ET-009 — все 3 enabled источника попадают в default-набор |
| `gps-tracks-color-mode` | `"source"` \| `"activity"` | без изменений |
### 4.2 Миграция defaults
При первом открытии страницы после ET-009 клиент видит, что
`gps-tracks-sources` (если есть в `localStorage` со старым значением
`["osm"]`) **не содержит** `enduro_russia` и `wikiloc`. Поведение
ET-008:
- Существующее значение `localStorage` сохраняется (пользователь
сознательно мог выключить источники);
- Новые источники появляются в UI-фильтре с галкой `unchecked`;
- Пользователь может включить их вручную.
Это **компромисс UX**: автоматическое включение новых источников
без согласия пользователя — нарушение принципа «без сюрпризов»;
оставляем явный opt-in.
При желании оператора (нет в scope ET-009) — добавить one-shot
migration в client-side JS: «если `gps-tracks-sources` существует и не
содержит `enduro_russia` или `wikiloc` — добавить и пересохранить».
**Не делаем в ET-009.**
### 4.3 Не-персистентное состояние
`window.gpsTracksLayer` (ET-008) — без изменений.
Маппинг `SOURCE_ATTRIBUTIONS` в `gps_tracks.js` расширяется:
```js
const SOURCE_ATTRIBUTIONS = {
osm: "© OpenStreetMap contributors (ODbL)",
enduro_russia: "EnduroRussia.ru",
wikiloc: "© Wikiloc contributors",
ttrails: "ttrails.ru", // для будущей активации
};
```
И маппинг `SOURCE_LABELS` для UI-чекбоксов:
```js
const SOURCE_LABELS = {
osm: "OSM",
enduro_russia: "EnduroRussia",
wikiloc: "Wikiloc",
ttrails: "ttrails.ru",
};
```
## 5. Внешние входные данные
### 5.1 OSM Public GPS Traces (ADR-009) — без изменений
См. `docs/work-items/ET-008/08-data-requirements.md` §5.1.
### 5.2 EnduroRussia.ru (ADR-010 accepted)
| Параметр | Значение |
|---|---|
| Endpoint list | `GET https://endurorussia.ru/api/tracks?page=N&limit=50` |
| Endpoint GPX | `GET https://endurorussia.ru/api/tracks/{id}/gpx` |
| Формат list | JSON `{items: [{id, name, difficulty, created_at}, ...], total}` |
| Формат GPX | XML (GPX 1.1) — `<trk><trkseg><trkpt>` |
| Лицензия | Public; ADR-010 §3 — обезличенно (без `user`) |
| Атрибуция | `EnduroRussia.ru` |
| Rate-limit | 5 sec / req |
| Объём для ЦФО+Чувашии (оценка) | ≥ 200 треков |
| User-Agent | `enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)` |
| Authentication | Нет |
### 5.3 Wikiloc (ADR-012 accepted)
| Параметр | Значение |
|---|---|
| Endpoint поиска | `GET https://www.wikiloc.com/wikiloc/find.do?act=<code>&sw=<lat,lon>&ne=<lat,lon>&page=<N>` |
| Endpoint трека | `GET https://www.wikiloc.com/trails/<slug>/<id>` |
| Endpoint GPX | `GET https://www.wikiloc.com/wikiloc/downloadTrail.do?id=<id>` |
| Формат поиска | HTML (regex-extract `<a href="/trails/…/<id>">`) |
| Формат трека | HTML (regex-extract `<h1>` для имени + ссылка на GPX) |
| Формат GPX | XML (GPX 1.1) |
| Лицензия | Proprietary (ADR-012 §3 — обезличенно, без description) |
| Атрибуция | `© Wikiloc contributors` |
| Rate-limit | **10 sec / req** (жёстко) |
| Graceful-stop | На 403/429 — `return` без `raise` |
| max_tracks_per_run | 50 (soft-cap первого прогона) |
| User-Agent | `enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)` |
| Authentication | Нет |
### 5.4 ttrails.ru (ADR-011 proposed)
**Не используется в ET-009.** `enabled: false` в `gps_sources.yaml`,
pipeline-guard пропускает.
## 6. Контракт публичного API
### 6.1 `GET /api/gps-tracks` — без изменений
Endpoint остаётся как в ET-008. Новые ID источников
(`enduro_russia`, `wikiloc`) появляются в значениях:
- `properties.sources` — массив `["enduro_russia"]` / `["wikiloc"]` /
`["osm", "enduro_russia"]` (после dedup-merge);
- `properties.external_urls``["https://endurorussia.ru/tracks/<id>"]` /
`["https://www.wikiloc.com/trails/<slug>/<id>"]`.
**Никаких новых query-параметров, response-полей или error-кодов.**
Query-параметр `source=...` (фильтр по source ID) уже существует;
теперь принимает новые значения `enduro_russia`, `wikiloc`.
### 6.2 `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` — без изменений
`properties.source` в MVT-feature может теперь принимать значения
`enduro_russia` / `wikiloc` (первый source в `sources_json`).
Клиент-стиль (match-expression `line-color`) переключается на
соответствующий цвет.
### 6.3 `GET /api/gps-tracks/health` — без изменений в схеме
Response shape без изменений. Содержимое:
- `tracks_by_source` теперь содержит ключи `enduro_russia` и `wikiloc`
с числовыми значениями;
- `last_pipeline_run.sources_ok` / `sources_error` /
`sources_skipped_license` могут содержать новые source IDs.
Клиент-side `SOURCE_ATTRIBUTIONS` маппинг превращает ключи
`tracks_by_source` в строки атрибуции для MapLibre Attribution control
(REQ-F-14).
### 6.4 `POST /api/gps-tracks/cache/clear` — без изменений
## 7. Персональные данные (PII)
Без изменений vs ET-008 §7, с расширением табличного сводного:
| Канал | PII | Условия в ET-009 |
|---|---|---|
| `tracks.user` для `enduro_russia` | **нет**`save_user_field: false` (ADR-010) | сохраняется null |
| `tracks.user` для `wikiloc` | **нет**`save_user_field: false` (ADR-012) | сохраняется null |
| `tracks.geom`, `tracks.created_at`, `tracks.length_m` | низкий риск, публично выложено автором | сохраняется как в ET-008 |
| `tracks.description` для `enduro_russia` | возможны следы PII в свободном тексте | сохраняется в default (ADR-010 §3); может быть пере-включено `save_description: false` |
| `tracks.description` для `wikiloc` | возможны следы PII | **null**`save_description: false` (ADR-012) |
| `tracks.name` для `enduro_russia` / `wikiloc` | название может содержать псевдонимы | сохраняется (видно в popup) |
| IP mva154 становится известен `endurorussia.ru`, `wikiloc.com` | да | стандартное поведение скрейпера; User-Agent с контактом |
### 7.1 Право на удаление
Без изменений. `external_urls_json` хранит ссылку; точечное удаление
по запросу автора возможно (ET-008 §7.1).
### 7.2 GDPR / РФ ФЗ-152
Без изменений. Обрабатываются только публично выложенные данные.
## 8. Атрибуция
**Расширение vs ET-008:**
Источник | Атрибуция-строка |
|---|---|
| `osm` | `© OpenStreetMap contributors (ODbL)` |
| `enduro_russia` | `EnduroRussia.ru` |
| `wikiloc` | `© Wikiloc contributors` |
| `ttrails` (будущее) | `ttrails.ru` |
Клиент формирует список из `tracks_by_source` (где count > 0) через
`SOURCE_ATTRIBUTIONS` маппинг и подмешивает в MapLibre Attribution
control при включённом слое «Публичные треки».
В **popup трека** (`gps_tracks.js`) — ссылки `external_urls` (как в
ET-008 REQ-F-18); никаких дополнительных правок.
## 9. Backup и retention
Без изменений vs ET-008 §9. Ежедневный snapshot + 14 дней retention
для `data/gps_tracks.sqlite`. После ET-009 backup-размер вырастет с
~5 МБ до ~50 МБ — пренебрежимое влияние на disk budget.
## 10. Тестовые данные (фикстуры)
ET-009 вводит новые фикстуры в `tests/fixtures/gps-tracks/`:
| Файл | Содержимое | Использование |
|---|---|---|
| `enduro-russia-api-tracks-page1.json` | реальный snapshot `GET /api/tracks?page=0&limit=50`; ≥ 5 items с полями id/name/difficulty/created_at | UT-ER-01..08, IT-ER-01 |
| `enduro-russia-track-1.gpx` | реальный GPX, ≥ 10 trkpt, в bbox `tsfo_plus_chuvashia` | UT-ER-01, IT-ER-01 |
| `enduro-russia-track-2.gpx` | пустой GPX (`<trkseg></trkseg>`) | UT-ER-02 (skip-логика) |
| `enduro-russia-track-3.gpx` | GPX с одной точкой за пределами bbox | UT-ER-03 (bbox-фильтрация) |
| `wikiloc-search-page1.html` | HTML страницы поиска; ≥ 5 ссылок `/trails/…/<id>` | UT-WL-01, IT-WL-01 |
| `wikiloc-trail-page.html` | HTML страницы одного трека | UT-WL-02..04, IT-WL-01 |
| `wikiloc-track.gpx` | реальный GPX, координаты совпадают с одним из EnduroRussia-треков | UT-WL-05, IT-DEDUP-01 |
| `wikiloc-rate-limited.html` | пустой/тестовый HTML | UT-WL-07/08 (для mock 403/429) |
**Снимки делаются разово, вручную** оператором / разработчиком через
`curl` или браузер-инспектор; сохраняются в git и не зависят от
состояния сайта.
### 10.1 Юридический статус фикстур
Фикстуры в `tests/fixtures/gps-tracks/` — публичные snapshot'ы
открытых страниц/API, размещённые исключительно для **верификации
парсеров** (некоммерческое тестовое использование). Не включаются в
production-БД, не отдаются через API. Внутри фикстур не сохраняются
authentication-cookies, авторские контактные данные или иные PII.
При запросе администратора платформы — фикстура подменяется на
синтетический минимальный пример с той же структурой.
## 11. Контракты, которые нельзя ломать
Без изменений vs ET-008 §10:
1. `dedup_key` формула — не меняется в ET-009.
2. `ACTIVITY_TYPES` enum — не меняется в ET-009.
3. GeoJSON response shape — не меняется.
4. MVT layer name `gps_tracks` и properties — не меняется.
5. localStorage keys — не меняется.
**Новое**: маппинги `SOURCE_ATTRIBUTIONS` / `SOURCE_LABELS` в клиенте
являются «soft contract»: добавление ключей — safe; удаление —
сломает атрибуцию для соответствующих треков.
## 12. Вывод
ET-009 — **append-only data event**:
- Заполняет существующую схему БД новыми записями;
- Использует существующие API-контракты без изменений;
- Расширяет существующие client-side маппинги (атрибуция, цвета);
- Никаких миграций, никаких ALTER, никаких новых ключей localStorage.
Юридически защищён через ADR-010 (accepted) и ADR-012 (accepted).
Pipeline-guard прозрачен — `proposed` ADR блокирует source автоматически.

View File

@@ -0,0 +1,337 @@
---
type: tech-risks
work_item_id: ET-009
title: "Технические риски — ET-009: Активация EnduroRussia + Wikiloc"
version: 1
status: approved
created_at: 2026-06-01
authors:
- "agent:architect"
---
# Технические риски — ET-009
Технические риски этапа активации двух новых GPS-источников. Бизнес-риски —
в BRD §6 ET-009. Многие риски наследуются от ET-008 (R-1, R-5, R-9 из
`docs/work-items/ET-008/10-tech-risks.md`); здесь — специфика ET-009.
Шкала: вероятность (Н/С/В) × влияние (Н/С/В).
## R-1 — Wikiloc меняет HTML → парсер возвращает 0 треков
- **Описание:** Парсер Wikiloc опирается на regex-извлечение
`<a href="/trails/…/<id>">` и `<h1>` для названия. Wikiloc может в
любой момент изменить разметку (новый шаблон, JS-rendering) → парсер
вернёт 0 треков.
- **Вероятность / Влияние:** В / С.
- **Митигация:**
- Парсер уже спроектирован **graceful**: `return` без `raise` при
отсутствии match'ей regex (см. `wikiloc.py::_extract_track_paths`).
- Health-эндпоинт показывает `tracks_by_source.wikiloc = 0` после
прогона → видимый сигнал оператору.
- Unit-тест UT-WL-01 на снимке `wikiloc-search-page1.html` — при
смене разметки CI зелёным быть не сможет, разработчик обновит
фикстуру + парсер за 1 итерацию.
- `gps_sources.yaml::wikiloc.enabled: false` — мгновенное отключение
без deploy при критической поломке.
- **Наследник от:** ET-008 R-1 (general).
## R-2 — Wikiloc банит IP mva154
- **Описание:** Скрейпер с фиксированного IP может попасть в чёрный
список Wikiloc'а (особенно при ошибках rate-limit или
накоплении 1000+ запросов в сутки). Pipeline начнёт получать 403/429
на все запросы → новых треков не будет.
- **Вероятность / Влияние:** С / В.
- **Митигация:**
- `rate_limit_sec: 10` — самый консервативный rate в проекте.
- `max_tracks_per_run: 50` — soft-cap на первом прогоне; ≤ 150
запросов на одну активацию.
- `User-Agent` с контактным URL — платформа может связаться
через email до бана.
- **Graceful-stop** на 403/429 — не агрессивный retry, не вызывает
дополнительных запросов.
- **Мониторинг первых 3 прогонов** оператором; при систематических
403 → `enabled: false` + новый ADR-update «Wikiloc deprecated».
- Запрет использования прокси через сторонний IP (нарушает дух
прозрачности; см. ET-008 R-5).
## R-3 — EnduroRussia API меняет схему ответа
- **Описание:** `enduro_russia.py::_parse_gpx` ожидает поля
`id`, `name`, `difficulty`, `created_at` в JSON-items. Платформа
может добавить/переименовать поля.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- Парсер использует `.get()` с дефолтами — отсутствие необязательных
полей не валит.
- Отсутствие `id` → запись пропускается (`continue`), не валит весь
прогон.
- Контрактный smoke-тест `tests/contract/test_endurorussia_api_smoke.py`
с маркером `@pytest.mark.network` — запускается nightly или вручную,
сигнализирует о поломке внешнего API до пропущенного cron-прогона.
- Pipeline-error не лоадит всю БД: `errors_json` фиксирует, оператор
видит через `/health`.
## R-4 — Расхождение конфига `enduro-russia.ru` (с дефисом) vs
реального `endurorussia.ru` (без дефиса)
- **Описание:** До ET-009 `gps_sources.yaml::enduro_russia.base_url`
содержит `https://enduro-russia.ru` (с дефисом), но реальный
домен — `https://endurorussia.ru` (без дефиса; парсер по default
использует именно его). При активации `enabled: true` без фикса URL
парсер использовал бы default из кода, но `external_url` сохранённых
треков опирался бы на `base_url` из конфига → fragmentation
external_url'ов между «корректным» и «дефис-вариантом».
- **Вероятность / Влияние:** Случилось (известный bug в конфиге) /
В (при активации).
- **Митигация:**
- **F-01 в BRD/TRZ** — фикс URL в одно изменение.
- **Регрессионный тест UT-ER-05** — проверяет, что парсер
сохраняет URL без дефиса при передаче `base_url` без дефиса.
- One-shot UPDATE для существующих треков (опционально, см.
`07-infra-requirements.md` §4.1).
## R-5 — EnduroRussia и Wikiloc — двойник одного и того же трека → массовые дубли
- **Описание:** Авторы часто публикуют одну и ту же поездку и на
Wikiloc, и на EnduroRussia (Wikiloc даже сохраняет `creator=Wikiloc`
в GPX мета-теге, что подтверждается на практике). Без правильно
работающего dedup'а в БД появятся два трека с одинаковой геометрией.
- **Вероятность / Влияние:** В / С.
- **Митигация:**
- `compute_dedup_key` (ADR-006) основан на `bbox+length+date`, который
при достаточно похожих координатах и одной дате попадает в один
bucket → upsert ON CONFLICT мержит.
- **Интеграционный тест IT-DEDUP-01** — задаёт фикстуру `wikiloc-track.gpx`
с координатами, совпадающими с одним из EnduroRussia-треков; проверяет
итоговое объединение `sources_json=['enduro_russia','wikiloc']`.
- Метаданные при merge — берутся от source с большим `source_priority`
(`enduro_russia=80 > wikiloc=70`); `external_urls`оба сохраняются.
- Если на практике dedup пропускает (например, точное время / точный
bbox slightly off): план отступления ADR-006 §8 (сузить
length-bucket, добавить activity).
## R-6 — Cron первого прогона превышает окно из-за rate-limit Wikiloc
- **Описание:** При больших cap'ах `max_tracks_per_run` и rate-limit
10 сек × 3 запроса/трек первый прогон Wikiloc может занять часы.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
- `max_tracks_per_run: 50` — soft-cap → ≤ 25 минут на прогон Wikiloc.
- EnduroRussia при rate-limit 5 сек × 305 треков ≈ 25 минут — окей.
- Cron автоматизация **отложена** до отдельного DevOps-task'а
после двух успешных ручных прогонов; оператор контролирует
длительность.
- Опционально: `timeout 21600 docker compose ...` в cron (ET-008
R-11 уже фиксирует).
## R-7 — UI-фильтр «Источник» не подхватывает новые ID
- **Описание:** Если в ET-008 UI-фильтр (`#gps-source-grid`) построен
с захардкоженным списком `[osm]`, новые источники не появятся как
чекбоксы.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- **Дизайн ET-008**: UI строит фильтр **динамически** из ответа
`/api/gps-tracks/health.tracks_by_source` (источники с count > 0).
После первого прогона ET-009 — фильтр сам покажет 3 чекбокса.
- UI-тест TC-UI-04 (в `04b-ui-test-cases.md` ET-008) расширен для
ET-009: проверяет наличие 3 чекбоксов после двух прогонов.
- Маппинг `SOURCE_LABELS``gps_tracks.js`) расширяется явно
в ET-009 — даёт корректные читаемые названия.
## R-8 — Цветовая палитра в `style.json` / `style-dark.json` не содержит новых ID → линии серые
- **Описание:** В ET-008 match-expression `line-color` может содержать
только `osm`; новые источники получат fallback-серый.
- **Вероятность / Влияние:** В / Н.
- **Митигация:**
- **REQ-F-13** явно требует обновить match-expression с тремя
источниками + fallback.
- Code-review-чеклист: проверить наличие `enduro_russia`, `wikiloc`
в `paint.line-color` обоих стилей.
- При пропуске: визуальный регресс легко заметен в smoke-тесте
(TC-UI-05).
## R-9 — Дамп БД (резервная копия с старым URL) — orphan записи
- **Описание:** Если на test-сервере есть резервная копия БД, в которой
`external_urls_json` содержит `enduro-russia.ru` (с дефисом),
то после фикса URL новые treki будут иметь `endurorussia.ru` (без
дефиса), а старые — `enduro-russia.ru`. Это не криминал, но
фрагментация атрибуции.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- На практике `enduro_russia` до ET-009 был `enabled: false`
таких записей **нет**. Риск гипотетический.
- Опциональный one-shot `UPDATE tracks SET external_urls_json = REPLACE(...)`
— фиксируется в `14-deploy-log.md` если применяется.
## R-10 — ADR-010 / ADR-012 регрессировали в `proposed`
- **Описание:** Между моментом написания BRD/TRZ ET-009 и моментом
активации (merge → deploy) кто-то откатил статус ADR в `proposed`.
Pipeline-guard заблокирует source с `skipped_license`.
- **Вероятность / Влияние:** Н / В.
- **Митигация:**
- **F-03 / REQ-F-05** — pre-check перед активацией:
```bash
grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md
grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md
```
Оба должны вернуть `accepted`. Иначе — STOP и эскалация архитектору.
- Интеграционный тест IT-LIC-01 проверяет работу pipeline-guard'а:
подменяет `accepted → proposed` в копии ADR-010 и убеждается, что
pipeline скипает source с `status='skipped_license'`.
- **Наследник от:** ET-008 R-9.
## R-11 — Пользовательский opt-in для новых источников
- **Описание:** Пользователи с уже сохранённым `localStorage['gps-tracks-sources']
= ["osm"]` после ET-009 **не увидят** треки EnduroRussia/Wikiloc на
своих устройствах — клиент сохраняет старое значение, новые источники
по умолчанию не enabled в UI.
- **Вероятность / Влияние:** В / Н.
- **Митигация:**
- Это **сознательное решение** UX (см. `08-data-requirements.md` §4.2):
добавление источников без согласия пользователя — нарушение
принципа без сюрпризов.
- Чекбоксы новых источников появятся в `#sheet-gps-filters`
automatically (через health-endpoint), пользователь может включить
их вручную.
- В release-notes (если они есть в проекте) — фиксируем «появились
два новых источника, активация в фильтре».
## R-12 — Wikiloc-парсер сохраняет описание / автора несмотря на ADR-012
- **Описание:** ADR-012 §3 явно запрещает сохранять `description` и
`user` для Wikiloc. Если реализация парсера не уважает этот запрет
(например, `TrackInsert.description` заполняется), нарушение
licensing-условий.
- **Вероятность / Влияние:** Н / В.
- **Митигация:**
- **Текущая реализация:** `wikiloc.py::_parse_gpx` возвращает
`TrackInsert(description=None, user=None)` (зашито в коде).
- Unit-тест UT-WL-05 проверяет, что `description=None` и `user=None`
в возвращаемом `TrackInsert`.
- Code-review checklist в `12-review.md`: при любом изменении
парсера Wikiloc убедиться, что эти поля остаются null.
## R-13 — Тестовые фикстуры устаревают
- **Описание:** Снимки HTML/JSON, использованные в unit-тестах,
отражают состояние API/HTML **на момент снятия**. Через 6-12
месяцев платформа может изменить разметку, и фикстуры станут
неактуальны. Тесты пройдут (фикстура соответствует тесту), но
парсер **не будет работать** в production.
- **Вероятность / Влияние:** С / С.
- **Митигация:**
- Контрактный smoke-тест `test_endurorussia_api_smoke.py`
(`@pytest.mark.network`, nightly) — проверяет реальную схему
API, ловит расхождение.
- Аналогичный smoke для Wikiloc **не** делаем (риск бана IP при
регулярных запросах; ETC-009 §«REQ-F-16»).
- Health-эндпоинт показывает `tracks_by_source.wikiloc` после
каждого продакшн-прогона; устойчивое 0 — сигнал.
- При устаревании фикстуры — снимаем заново (1 час работы), парсер
обновляем (1-3 часа).
## R-14 — Производительность endpoint деградирует при росте кол-ва треков
- **Описание:** REQ-NF-02 ET-008 фиксирует p95 ≤ 300 мс на bbox с
≤ 500 треков. После ET-009 в БД появятся ещё ≤ 250 треков —
пренебрежимо относительно 5000 OSM.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- R-tree индекс по `geom` (ADR-005) → O(log n) bbox-prefetch.
- AC-20 — нагрузочный тест 100 запросов после первого прогона.
- При деградации — анализ EXPLAIN QUERY PLAN; добавление индекса
`idx_tracks_source` опционально (out of scope ET-009).
## R-15 — Конфликт MAPPING-таблиц для одной активности
- **Описание:** EnduroRussia маппит `motorcycle → moto`, Wikiloc
тоже `motorcycle → moto` — корректно. Но: EnduroRussia при
отсутствии match'а в MAPPING возвращает `enduro` (fallback),
Wikiloc — `moto`. Для одного и того же трека (попавшего в оба
источника) при merge получим `activity_type` от source с большим
`source_priority` = `enduro_russia` → `enduro`. Это **OK**: priority
делает выбор детерминированным.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
- Принято as is. Уточнение в `08-data-requirements.md` §3.4.
- Unit-тесты UT-ER-04 и UT-WL-06 проверяют отдельные MAPPING'и.
## R-16 — Регрессия e2e-тестов ET-008
- **Описание:** Расширение `style.json` / `gps_tracks.js`
атрибуцией и цветами может случайно сломать существующие
selectors / визуальные тесты ET-008.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
- **AC-19** — все e2e ET-008 (E-01..E-41) должны пройти после
мерджа ET-009.
- Регрессионный прогон `pytest tests/e2e/ -v` — обязательный
шаг CI.
## R-17 — Pipeline скипает source из-за неправильного `license_adr` path
- **Описание:** Pipeline-guard `_check_license_adr` читает YAML
front-matter файла по пути из `license_adr`. Если путь опечатан
(например, `ADR-12-...` вместо `ADR-012-...`), guard вернёт false →
`status='skipped_license'`.
- **Вероятность / Влияние:** Н / В.
- **Митигация:**
- Pre-deploy check: убедиться, что `license_adr` указывает на
реально существующий файл с `status: accepted`.
- При первом запуске pipeline в test-среде оператор смотрит
`pipeline_runs[-1].status`; если `skipped_license` —
диагностирует и исправляет до merge в main.
- Pydantic-валидация `gps_sources.yaml` в pipeline ET-008 уже
требует обязательное `license_adr` поле; отсутствие — exception
при старте.
- **Наследник от:** ET-008 R-9.
## Сводная таблица
| ID | Риск | Вер. | Влияние | Класс | Статус |
|---|---|---|---|---|---|
| R-1 | Wikiloc меняет HTML | В | С | Высокий | принят + graceful + быстрое отключение |
| R-2 | Wikiloc банит IP mva154 | С | В | Высокий | rate-limit 10s + cap 50 + UA + monitor |
| R-3 | EnduroRussia API меняет схему | Н | С | Низкий | smoke-тест + graceful + health |
| R-4 | Расхождение URL `enduro-russia` vs `endurorussia` | Случилось | В | Высокий | F-01 фикс + UT-ER-05 |
| R-5 | Дубли EnduroRussia/Wikiloc | В | С | Средний | dedup-key + IT-DEDUP-01 |
| R-6 | Cron первого прогона долго | С | Н | Низкий | `max_tracks_per_run=50` + ручной прогон |
| R-7 | UI-фильтр не подхватит | Н | С | Низкий | динамика из health + SOURCE_LABELS |
| R-8 | Стили без новых цветов | В | Н | Низкий | REQ-F-13 + review + smoke |
| R-9 | Orphan записи с старым URL | Н | Н | Низкий | гипотетический (БД чистая); опц UPDATE |
| R-10 | ADR-010/012 регрессировали в proposed | Н | В | Высокий | pre-check + IT-LIC-01 |
| R-11 | Пользовательский opt-in для новых источников | В | Н | Низкий | сознательный UX-compromise |
| R-12 | Wikiloc сохраняет description/user | Н | В | Высокий | parser-design + UT-WL-05 + review |
| R-13 | Фикстуры устаревают | С | С | Средний | smoke-test + health + ручной refresh |
| R-14 | Деградация endpoint | Н | Н | Низкий | R-tree + AC-20 |
| R-15 | Конфликт MAPPING | Н | Н | Низкий | source_priority детерминирует |
| R-16 | Регрессия ET-008 e2e | Н | С | Низкий | AC-19 + pytest e2e |
| R-17 | Неправильный `license_adr` path | Н | В | Высокий | pre-deploy check + Pydantic |
**Высокие классы:**
- R-1, R-2 — операционные, ожидаемые для скрейп-источника Wikiloc;
митигация — multi-layer (graceful + monitor + конфиг-kill-switch).
- R-4 — known bug в конфиге, прямо адресован REQ-F-01.
- R-10, R-17 — критичны для legal compliance; митигация многослойная
(pre-check + integration-тест + Pydantic).
- R-12 — критичен для соблюдения ADR-012; митигация через design +
UT-WL-05 + review.
**Блокирующих рисков нет.** Высокие классы требуют внимания на этапе
разработки и code review.
## Эскалация
- **arch:major-change** — **не выставляется** (см. ADR-013
§«Классификация»). Изменение не вводит новых архитектурных компонентов.
- **back-to:analysis** — не требуется. ТЗ полное, BRD ясный,
open-questions в `TRZ §6` закрыты дефолтными решениями.
- Эскалация архитектору требуется **только** при срабатывании R-10
(ADR в `proposed` на момент активации). Тогда задача останавливается
до повторного апрува ADR.