diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3f71c..a1c58f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,19 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [Unreleased] ### Added +- ET-009: Активация GPS-источников EnduroRussia и Wikiloc — `config/gps_sources.yaml` + включает оба источника (`enabled: true`), для Wikiloc добавлен soft-cap + `max_tracks_per_run: 50` и activity-фильтр; `config/gps_regions.yaml` подписывает + `wikiloc` на регион `tsfo_plus_chuvashia`. Парсер `wikiloc.py` извлекает время из + GPX-metadata (для корректной дедупликации) и поддерживает `max_tracks_per_run` + cap. UI: цвет `wikiloc`, чекбокс источника, динамическая атрибуция + (`GPS_SOURCE_ATTRIBUTIONS`) подтягивается с `/api/gps-tracks/health`. + Тесты: 10 unit ER + 10 unit WL + 5 integration + 2 contract (nightly only). + +### Fixed +- ET-009: исправлен URL `enduro_russia` в `config/gps_sources.yaml` + (`https://enduro-russia.ru` → `https://endurorussia.ru`, без дефиса). + - Initial project structure - CLAUDE.md project passport - Agent system prompts (architect, developer, reviewer, tester, deployer) diff --git a/config/gps_regions.yaml b/config/gps_regions.yaml index ba80554..dd276ed 100644 --- a/config/gps_regions.yaml +++ b/config/gps_regions.yaml @@ -3,7 +3,7 @@ regions: name: "ЦФО + Чувашия" bbox: [29.0, 49.5, 47.5, 60.0] enabled: true - sources: [osm, enduro_russia, ttrails] + sources: [osm, enduro_russia, wikiloc, ttrails] - id: north_caucasus name: "Северный Кавказ" diff --git a/config/gps_sources.yaml b/config/gps_sources.yaml index 98dc559..a8fd78c 100644 --- a/config/gps_sources.yaml +++ b/config/gps_sources.yaml @@ -13,14 +13,29 @@ sources: - id: enduro_russia name: "EnduroRussia.ru" - enabled: false + enabled: true license_adr: "docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md" - base_url: "https://enduro-russia.ru" + base_url: "https://endurorussia.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 + 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: "Тропинки.ру" diff --git a/pyproject.toml b/pyproject.toml index 9e1a6c9..03bb965 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,7 @@ line-length = 120 [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] +markers = [ + "network: contract smoke tests that hit live HTTP endpoints (deselect with '-m \"not network\"')", +] +addopts = "-m 'not network'" diff --git a/src/api/gps_tracks/sources/wikiloc.py b/src/api/gps_tracks/sources/wikiloc.py index a26dbe6..b839100 100644 --- a/src/api/gps_tracks/sources/wikiloc.py +++ b/src/api/gps_tracks/sources/wikiloc.py @@ -62,6 +62,8 @@ class WikilocParser(SourceParser): source_id = self.config.get("id", "wikiloc") source_priority = self.config.get("source_priority", 70) activity_filter = self.config.get("activity_filter", ["motorcycle", "enduro"]) + max_tracks = self.config.get("max_tracks_per_run") + yielded = 0 headers = { "User-Agent": user_agent, @@ -188,7 +190,15 @@ class WikilocParser(SourceParser): ): continue + if max_tracks is not None and yielded >= max_tracks: + logger.info( + "Wikiloc: reached max_tracks_per_run=%d, stopping", + max_tracks, + ) + return + yield track + yielded += 1 page += 1 @@ -260,16 +270,40 @@ def _parse_gpx( if tag.startswith("{"): ns = tag.split("}")[0] + "}" - # Извлекаем название из GPX metadata если нет из HTML - if not name: - for child in root: - local = child.tag.replace(ns, "") if ns else child.tag - if local == "metadata": - for meta_child in child: - local2 = meta_child.tag.replace(ns, "") if ns else meta_child.tag - if local2 == "name": - name = meta_child.text + # Извлекаем название и время из GPX metadata + created_at = None + for child in root: + local = child.tag.replace(ns, "") if ns else child.tag + if local == "metadata": + for meta_child in child: + local2 = meta_child.tag.replace(ns, "") if ns else meta_child.tag + if local2 == "name" and not name: + name = meta_child.text + elif local2 == "time" and meta_child.text: + created_at = meta_child.text.strip() + break + + # Fallback: первая